You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by gc...@apache.org on 2023/01/17 18:09:11 UTC

[allura] 01/01: [#8491] upgraded tablesorter.js, minified sylvester.js and code update for tablesorter

This is an automated email from the ASF dual-hosted git repository.

gcruz pushed a commit to branch gc/8491
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 6ff740afe727f2515739a2a620d4ad11aa20a056
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Tue Jan 17 12:08:29 2023 -0600

    [#8491] upgraded tablesorter.js, minified sylvester.js and code update for tablesorter
---
 Allura/allura/public/nf/js/jquery.tablesorter.js   | 3947 +++++++++++++++-----
 Allura/allura/public/nf/js/sylvester.js            |    2 +-
 .../allura/templates/site_admin_new_projects.html  |   10 +-
 3 files changed, 2926 insertions(+), 1033 deletions(-)

diff --git a/Allura/allura/public/nf/js/jquery.tablesorter.js b/Allura/allura/public/nf/js/jquery.tablesorter.js
index 9b5873122..5be845f32 100644
--- a/Allura/allura/public/nf/js/jquery.tablesorter.js
+++ b/Allura/allura/public/nf/js/jquery.tablesorter.js
@@ -1,1031 +1,2916 @@
-/*
- * 
- * TableSorter 2.0 - Client-side table sorting with ease!
- * Version 2.0.5b
- * @requires jQuery v1.2.3
- * 
- * Copyright (c) 2007 Christian Bach
- * Examples and docs at: http://tablesorter.com
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- * 
- */
-/**
- * 
- * @description Create a sortable table with multi-column sorting capabilitys
- * 
- * @example $('table').tablesorter();
- * @desc Create a simple tablesorter interface.
- * 
- * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] });
- * @desc Create a tablesorter interface and sort on the first and secound column column headers.
- * 
- * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } });
- *          
- * @desc Create a tablesorter interface and disableing the first and second  column headers.
- *      
- * 
- * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } });
- * 
- * @desc Create a tablesorter interface and set a column parser for the first
- *       and second column.
- * 
- * 
- * @param Object
- *            settings An object literal containing key/value pairs to provide
- *            optional settings.
- * 
- * 
- * @option String cssHeader (optional) A string of the class name to be appended
- *         to sortable tr elements in the thead of the table. Default value:
- *         "header"
- * 
- * @option String cssAsc (optional) A string of the class name to be appended to
- *         sortable tr elements in the thead on a ascending sort. Default value:
- *         "headerSortUp"
- * 
- * @option String cssDesc (optional) A string of the class name to be appended
- *         to sortable tr elements in the thead on a descending sort. Default
- *         value: "headerSortDown"
- * 
- * @option String sortInitialOrder (optional) A string of the inital sorting
- *         order can be asc or desc. Default value: "asc"
- * 
- * @option String sortMultisortKey (optional) A string of the multi-column sort
- *         key. Default value: "shiftKey"
- * 
- * @option String textExtraction (optional) A string of the text-extraction
- *         method to use. For complex html structures inside td cell set this
- *         option to "complex", on large tables the complex option can be slow.
- *         Default value: "simple"
- * 
- * @option Object headers (optional) An array containing the forces sorting
- *         rules. This option let's you specify a default sorting rule. Default
- *         value: null
- * 
- * @option Array sortList (optional) An array containing the forces sorting
- *         rules. This option let's you specify a default sorting rule. Default
- *         value: null
- * 
- * @option Array sortForce (optional) An array containing forced sorting rules.
- *         This option let's you specify a default sorting rule, which is
- *         prepended to user-selected rules. Default value: null
- * 
- * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever
- *         to use String.localeCampare method or not. Default set to true.
- * 
- * 
- * @option Array sortAppend (optional) An array containing forced sorting rules.
- *         This option let's you specify a default sorting rule, which is
- *         appended to user-selected rules. Default value: null
- * 
- * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter
- *         should apply fixed widths to the table columns. This is usefull when
- *         using the pager companion plugin. This options requires the dimension
- *         jquery plugin. Default value: false
- * 
- * @option Boolean cancelSelection (optional) Boolean flag indicating if
- *         tablesorter should cancel selection of the table headers text.
- *         Default value: true
- * 
- * @option Boolean debug (optional) Boolean flag indicating if tablesorter
- *         should display debuging information usefull for development.
- * 
- * @type jQuery
- * 
- * @name tablesorter
- * 
- * @cat Plugins/Tablesorter
- * 
- * @author Christian Bach/christian.bach@polyester.se
- */
-
-(function ($) {
-    $.extend({
-        tablesorter: new
-        function () {
-
-            var parsers = [],
-                widgets = [];
-
-            this.defaults = {
-                cssHeader: "header",
-                cssAsc: "headerSortUp",
-                cssDesc: "headerSortDown",
-                cssChildRow: "expand-child",
-                sortInitialOrder: "asc",
-                sortMultiSortKey: "shiftKey",
-                sortForce: null,
-                sortAppend: null,
-                sortLocaleCompare: true,
-                textExtraction: "simple",
-                parsers: {}, widgets: [],
-                widgetZebra: {
-                    css: ["even", "odd"]
-                }, headers: {}, widthFixed: false,
-                cancelSelection: true,
-                sortList: [],
-                headerList: [],
-                dateFormat: "us",
-                decimal: '/\.|\,/g',
-                onRenderHeader: null,
-                selectorHeaders: 'thead th',
-                debug: false
-            };
-
-            /* debuging utils */
-
-            function benchmark(s, d) {
-                log(s + "," + (new Date().getTime() - d.getTime()) + "ms");
-            }
-
-            this.benchmark = benchmark;
-
-            function log(s) {
-                if (typeof console != "undefined" && typeof console.debug != "undefined") {
-                    console.log(s);
-                } else {
-                    alert(s);
-                }
-            }
-
-            /* parsers utils */
-
-            function buildParserCache(table, $headers) {
-
-                if (table.config.debug) {
-                    var parsersDebug = "";
-                }
-
-                if (table.tBodies.length == 0) return; // In the case of empty tables
-                var rows = table.tBodies[0].rows;
-
-                if (rows[0]) {
-
-                    var list = [],
-                        cells = rows[0].cells,
-                        l = cells.length;
-
-                    for (var i = 0; i < l; i++) {
-
-                        var p = false;
-
-                        if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) {
-
-                            p = getParserById($($headers[i]).metadata().sorter);
-
-                        } else if ((table.config.headers[i] && table.config.headers[i].sorter)) {
-
-                            p = getParserById(table.config.headers[i].sorter);
-                        }
-                        if (!p) {
-
-                            p = detectParserForColumn(table, rows, -1, i);
-                        }
-
-                        if (table.config.debug) {
-                            parsersDebug += "column:" + i + " parser:" + p.id + "\n";
-                        }
-
-                        list.push(p);
-                    }
-                }
-
-                if (table.config.debug) {
-                    log(parsersDebug);
-                }
-
-                return list;
-            };
-
-            function detectParserForColumn(table, rows, rowIndex, cellIndex) {
-                var l = parsers.length,
-                    node = false,
-                    nodeValue = false,
-                    keepLooking = true;
-                while (nodeValue == '' && keepLooking) {
-                    rowIndex++;
-                    if (rows[rowIndex]) {
-                        node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex);
-                        nodeValue = trimAndGetNodeText(table.config, node);
-                        if (table.config.debug) {
-                            log('Checking if value was empty on row:' + rowIndex);
-                        }
-                    } else {
-                        keepLooking = false;
-                    }
-                }
-                for (var i = 1; i < l; i++) {
-                    if (parsers[i].is(nodeValue, table, node)) {
-                        return parsers[i];
-                    }
-                }
-                // 0 is always the generic parser (text)
-                return parsers[0];
-            }
-
-            function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) {
-                return rows[rowIndex].cells[cellIndex];
-            }
-
-            function trimAndGetNodeText(config, node) {
-                return $.trim(getElementText(config, node));
-            }
-
-            function getParserById(name) {
-                var l = parsers.length;
-                for (var i = 0; i < l; i++) {
-                    if (parsers[i].id.toLowerCase() == name.toLowerCase()) {
-                        return parsers[i];
-                    }
-                }
-                return false;
-            }
-
-            /* utils */
-
-            function buildCache(table) {
-
-                if (table.config.debug) {
-                    var cacheTime = new Date();
-                }
-
-                var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0,
-                    totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0,
-                    parsers = table.config.parsers,
-                    cache = {
-                        row: [],
-                        normalized: []
-                    };
-
-                for (var i = 0; i < totalRows; ++i) {
-
-                    /** Add the table data to main data array */
-                    var c = $(table.tBodies[0].rows[i]),
-                        cols = [];
-
-                    // if this is a child row, add it to the last row's children and
-                    // continue to the next row
-                    if (c.hasClass(table.config.cssChildRow)) {
-                        cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c);
-                        // go to the next for loop
-                        continue;
-                    }
-
-                    cache.row.push(c);
-
-                    for (var j = 0; j < totalCells; ++j) {
-                        cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j]));
-                    }
-
-                    cols.push(cache.normalized.length); // add position for rowCache
-                    cache.normalized.push(cols);
-                    cols = null;
-                };
-
-                if (table.config.debug) {
-                    benchmark("Building cache for " + totalRows + " rows:", cacheTime);
-                }
-
-                return cache;
-            };
-
-            function getElementText(config, node) {
-
-                var text = "";
-
-                if (!node) return "";
-
-                if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false;
-
-                if (config.textExtraction == "simple") {
-                    if (config.supportsTextContent) {
-                        text = node.textContent;
-                    } else {
-                        if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) {
-                            text = node.childNodes[0].innerHTML;
-                        } else {
-                            text = node.innerHTML;
-                        }
-                    }
-                } else {
-                    if (typeof(config.textExtraction) == "function") {
-                        text = config.textExtraction(node);
-                    } else {
-                        text = $(node).text();
-                    }
-                }
-                return text;
-            }
-
-            function appendToTable(table, cache) {
-
-                if (table.config.debug) {
-                    var appendTime = new Date()
-                }
-
-                var c = cache,
-                    r = c.row,
-                    n = c.normalized,
-                    totalRows = n.length,
-                    checkCell = (n[0].length - 1),
-                    tableBody = $(table.tBodies[0]),
-                    rows = [];
-
-
-                for (var i = 0; i < totalRows; i++) {
-                    var pos = n[i][checkCell];
-
-                    rows.push(r[pos]);
-
-                    if (!table.config.appender) {
-
-                        //var o = ;
-                        var l = r[pos].length;
-                        for (var j = 0; j < l; j++) {
-                            tableBody[0].appendChild(r[pos][j]);
-                        }
-
-                        // 
-                    }
-                }
-
-
-
-                if (table.config.appender) {
-
-                    table.config.appender(table, rows);
-                }
-
-                rows = null;
-
-                if (table.config.debug) {
-                    benchmark("Rebuilt table:", appendTime);
-                }
-
-                // apply table widgets
-                applyWidget(table);
-
-                // trigger sortend
-                setTimeout(function () {
-                    $(table).trigger("sortEnd");
-                }, 0);
-
-            };
-
-            function buildHeaders(table) {
-
-                if (table.config.debug) {
-                    var time = new Date();
-                }
-
-                var meta = ($.metadata) ? true : false;
-                
-                var header_index = computeTableHeaderCellIndexes(table);
-
-                $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) {
-
-                    this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex];
-                    // this.column = index;
-                    this.order = formatSortingOrder(table.config.sortInitialOrder);
-                    
-					
-					this.count = this.order;
-
-                    if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true;
-					if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index);
-
-                    if (!this.sortDisabled) {
-                        var $th = $(this).addClass(table.config.cssHeader);
-                        if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th);
-                    }
-
-                    // add cell to headerList
-                    table.config.headerList[index] = this;
-                });
-
-                if (table.config.debug) {
-                    benchmark("Built headers:", time);
-                    log($tableHeaders);
-                }
-
-                return $tableHeaders;
-
-            };
-
-            // from:
-            // http://www.javascripttoolbox.com/lib/table/examples.php
-            // http://www.javascripttoolbox.com/temp/table_cellindex.html
-
-
-            function computeTableHeaderCellIndexes(t) {
-                var matrix = [];
-                var lookup = {};
-                var thead = t.getElementsByTagName('THEAD')[0];
-                var trs = thead.getElementsByTagName('TR');
-
-                for (var i = 0; i < trs.length; i++) {
-                    var cells = trs[i].cells;
-                    for (var j = 0; j < cells.length; j++) {
-                        var c = cells[j];
-
-                        var rowIndex = c.parentNode.rowIndex;
-                        var cellId = rowIndex + "-" + c.cellIndex;
-                        var rowSpan = c.rowSpan || 1;
-                        var colSpan = c.colSpan || 1
-                        var firstAvailCol;
-                        if (typeof(matrix[rowIndex]) == "undefined") {
-                            matrix[rowIndex] = [];
-                        }
-                        // Find first available column in the first row
-                        for (var k = 0; k < matrix[rowIndex].length + 1; k++) {
-                            if (typeof(matrix[rowIndex][k]) == "undefined") {
-                                firstAvailCol = k;
-                                break;
-                            }
-                        }
-                        lookup[cellId] = firstAvailCol;
-                        for (var k = rowIndex; k < rowIndex + rowSpan; k++) {
-                            if (typeof(matrix[k]) == "undefined") {
-                                matrix[k] = [];
-                            }
-                            var matrixrow = matrix[k];
-                            for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
-                                matrixrow[l] = "x";
-                            }
-                        }
-                    }
-                }
-                return lookup;
-            }
-
-            function checkCellColSpan(table, rows, row) {
-                var arr = [],
-                    r = table.tHead.rows,
-                    c = r[row].cells;
-
-                for (var i = 0; i < c.length; i++) {
-                    var cell = c[i];
-
-                    if (cell.colSpan > 1) {
-                        arr = arr.concat(checkCellColSpan(table, headerArr, row++));
-                    } else {
-                        if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) {
-                            arr.push(cell);
-                        }
-                        // headerArr[row] = (i+row);
-                    }
-                }
-                return arr;
-            };
-
-            function checkHeaderMetadata(cell) {
-                if (($.metadata) && ($(cell).metadata().sorter === false)) {
-                    return true;
-                };
-                return false;
-            }
-
-            function checkHeaderOptions(table, i) {
-                if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) {
-                    return true;
-                };
-                return false;
-            }
-			
-			 function checkHeaderOptionsSortingLocked(table, i) {
-                if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder;
-                return false;
-            }
-			
-            function applyWidget(table) {
-                var c = table.config.widgets;
-                var l = c.length;
-                for (var i = 0; i < l; i++) {
-
-                    getWidgetById(c[i]).format(table);
-                }
-
-            }
-
-            function getWidgetById(name) {
-                var l = widgets.length;
-                for (var i = 0; i < l; i++) {
-                    if (widgets[i].id.toLowerCase() == name.toLowerCase()) {
-                        return widgets[i];
-                    }
-                }
-            };
-
-            function formatSortingOrder(v) {
-                if (typeof(v) != "Number") {
-                    return (v.toLowerCase() == "desc") ? 1 : 0;
-                } else {
-                    return (v == 1) ? 1 : 0;
-                }
-            }
-
-            function isValueInArray(v, a) {
-                var l = a.length;
-                for (var i = 0; i < l; i++) {
-                    if (a[i][0] == v) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-
-            function setHeadersCss(table, $headers, list, css) {
-                // remove all header information
-                $headers.removeClass(css[0]).removeClass(css[1]);
-
-                var h = [];
-                $headers.each(function (offset) {
-                    if (!this.sortDisabled) {
-                        h[this.column] = $(this);
-                    }
-                });
-
-                var l = list.length;
-                for (var i = 0; i < l; i++) {
-                    h[list[i][0]].addClass(css[list[i][1]]);
-                }
-            }
-
-            function fixColumnWidth(table, $headers) {
-                var c = table.config;
-                if (c.widthFixed) {
-                    var colgroup = $('<colgroup>');
-                    $("tr:first td", table.tBodies[0]).each(function () {
-                        colgroup.append($('<col>').css('width', $(this).width()));
-                    });
-                    $(table).prepend(colgroup);
-                };
-            }
-
-            function updateHeaderSortCount(table, sortList) {
-                var c = table.config,
-                    l = sortList.length;
-                for (var i = 0; i < l; i++) {
-                    var s = sortList[i],
-                        o = c.headerList[s[0]];
-                    o.count = s[1];
-                    o.count++;
-                }
-            }
-
-            /* sorting methods */
-
-            function multisort(table, sortList, cache) {
-
-                if (table.config.debug) {
-                    var sortTime = new Date();
-                }
-
-                var dynamicExp = "var sortWrapper = function(a,b) {",
-                    l = sortList.length;
-
-                // TODO: inline functions.
-                for (var i = 0; i < l; i++) {
-
-                    var c = sortList[i][0];
-                    var order = sortList[i][1];
-                    // var s = (getCachedSortType(table.config.parsers,c) == "text") ?
-                    // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ?
-                    // "sortNumeric" : "sortNumericDesc");
-                    // var s = (table.config.parsers[c].type == "text") ? ((order == 0)
-                    // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ?
-                    // makeSortNumeric(c) : makeSortNumericDesc(c));
-                    var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c));
-                    var e = "e" + i;
-
-                    dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c
-                    // + "]); ";
-                    dynamicExp += "if(" + e + ") { return " + e + "; } ";
-                    dynamicExp += "else { ";
-
-                }
-
-                // if value is the same keep orignal order
-                var orgOrderCol = cache.normalized[0].length - 1;
-                dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];";
-
-                for (var i = 0; i < l; i++) {
-                    dynamicExp += "}; ";
-                }
-
-                dynamicExp += "return 0; ";
-                dynamicExp += "}; ";
-
-                if (table.config.debug) {
-                    benchmark("Evaling expression:" + dynamicExp, new Date());
-                }
-
-                eval(dynamicExp);
-
-                cache.normalized.sort(sortWrapper);
-
-                if (table.config.debug) {
-                    benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime);
-                }
-
-                return cache;
-            };
-
-            function makeSortFunction(type, direction, index) {
-                var a = "a[" + index + "]",
-                    b = "b[" + index + "]";
-                if (type == 'text' && direction == 'asc') {
-                    return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));";
-                } else if (type == 'text' && direction == 'desc') {
-                    return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));";
-                } else if (type == 'numeric' && direction == 'asc') {
-                    return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));";
-                } else if (type == 'numeric' && direction == 'desc') {
-                    return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));";
-                }
-            };
-
-            function makeSortText(i) {
-                return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));";
-            };
-
-            function makeSortTextDesc(i) {
-                return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));";
-            };
-
-            function makeSortNumeric(i) {
-                return "a[" + i + "]-b[" + i + "];";
-            };
-
-            function makeSortNumericDesc(i) {
-                return "b[" + i + "]-a[" + i + "];";
-            };
-
-            function sortText(a, b) {
-                if (table.config.sortLocaleCompare) return a.localeCompare(b);
-                return ((a < b) ? -1 : ((a > b) ? 1 : 0));
-            };
-
-            function sortTextDesc(a, b) {
-                if (table.config.sortLocaleCompare) return b.localeCompare(a);
-                return ((b < a) ? -1 : ((b > a) ? 1 : 0));
-            };
-
-            function sortNumeric(a, b) {
-                return a - b;
-            };
-
-            function sortNumericDesc(a, b) {
-                return b - a;
-            };
-
-            function getCachedSortType(parsers, i) {
-                return parsers[i].type;
-            }; /* public methods */
-            this.construct = function (settings) {
-                return this.each(function () {
-                    // if no thead or tbody quit.
-                    if (!this.tHead || !this.tBodies) return;
-                    // declare
-                    var $this, $document, $headers, cache, config, shiftDown = 0,
-                        sortOrder;
-                    // new blank config object
-                    this.config = {};
-                    // merge and extend.
-                    config = $.extend(this.config, $.tablesorter.defaults, settings);
-                    // store common expression for speed
-                    $this = $(this);
-                    // save the settings where they read
-                    $.data(this, "tablesorter", config);
-                    // build headers
-                    $headers = buildHeaders(this);
-                    // try to auto detect column type, and store in tables config
-                    this.config.parsers = buildParserCache(this, $headers);
-                    // build the cache for the tbody cells
-                    cache = buildCache(this);
-                    // get the css class names, could be done else where.
-                    var sortCSS = [config.cssDesc, config.cssAsc];
-                    // fixate columns if the users supplies the fixedWidth option
-                    fixColumnWidth(this);
-                    // apply event handling to headers
-                    // this is to big, perhaps break it out?
-                    $headers.click(
-
-                    function (e) {
-                        var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0;
-                        if (!this.sortDisabled && totalRows > 0) {
-                            // Only call sortStart if sorting is
-                            // enabled.
-                            $this.trigger("sortStart");
-                            // store exp, for speed
-                            var $cell = $(this);
-                            // get current column index
-                            var i = this.column;
-                            // get current column sort order
-                            this.order = this.count++ % 2;
-							// always sort on the locked order.
-							if(this.lockedOrder) this.order = this.lockedOrder;
-							
-							// user only whants to sort on one
-                            // column
-                            if (!e[config.sortMultiSortKey]) {
-                                // flush the sort list
-                                config.sortList = [];
-                                if (config.sortForce != null) {
-                                    var a = config.sortForce;
-                                    for (var j = 0; j < a.length; j++) {
-                                        if (a[j][0] != i) {
-                                            config.sortList.push(a[j]);
-                                        }
-                                    }
-                                }
-                                // add column to sort list
-                                config.sortList.push([i, this.order]);
-                                // multi column sorting
-                            } else {
-                                // the user has clicked on an all
-                                // ready sortet column.
-                                if (isValueInArray(i, config.sortList)) {
-                                    // revers the sorting direction
-                                    // for all tables.
-                                    for (var j = 0; j < config.sortList.length; j++) {
-                                        var s = config.sortList[j],
-                                            o = config.headerList[s[0]];
-                                        if (s[0] == i) {
-                                            o.count = s[1];
-                                            o.count++;
-                                            s[1] = o.count % 2;
-                                        }
-                                    }
-                                } else {
-                                    // add column to sort list array
-                                    config.sortList.push([i, this.order]);
-                                }
-                            };
-                            setTimeout(function () {
-                                // set css for headers
-                                setHeadersCss($this[0], $headers, config.sortList, sortCSS);
-                                appendToTable(
-	                                $this[0], multisort(
-	                                $this[0], config.sortList, cache)
-								);
-                            }, 1);
-                            // stop normal event by returning false
-                            return false;
-                        }
-                        // cancel selection
-                    }).mousedown(function () {
-                        if (config.cancelSelection) {
-                            this.onselectstart = function () {
-                                return false
-                            };
-                            return false;
-                        }
-                    });
-                    // apply easy methods that trigger binded events
-                    $this.bind("update", function () {
-                        var me = this;
-                        setTimeout(function () {
-                            // rebuild parsers.
-                            me.config.parsers = buildParserCache(
-                            me, $headers);
-                            // rebuild the cache map
-                            cache = buildCache(me);
-                        }, 1);
-                    }).bind("updateCell", function (e, cell) {
-                        var config = this.config;
-                        // get position from the dom.
-                        var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex];
-                        // update cache
-                        cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format(
-                        getElementText(config, cell), cell);
-                    }).bind("sorton", function (e, list) {
-                        $(this).trigger("sortStart");
-                        config.sortList = list;
-                        // update and store the sortlist
-                        var sortList = config.sortList;
-                        // update header count index
-                        updateHeaderSortCount(this, sortList);
-                        // set css for headers
-                        setHeadersCss(this, $headers, sortList, sortCSS);
-                        // sort the table and append it to the dom
-                        appendToTable(this, multisort(this, sortList, cache));
-                    }).bind("appendCache", function () {
-                        appendToTable(this, cache);
-                    }).bind("applyWidgetId", function (e, id) {
-                        getWidgetById(id).format(this);
-                    }).bind("applyWidgets", function () {
-                        // apply widgets
-                        applyWidget(this);
-                    });
-                    if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) {
-                        config.sortList = $(this).metadata().sortlist;
-                    }
-                    // if user has supplied a sort list to constructor.
-                    if (config.sortList.length > 0) {
-                        $this.trigger("sorton", [config.sortList]);
-                    }
-                    // apply widgets
-                    applyWidget(this);
-                });
-            };
-            this.addParser = function (parser) {
-                var l = parsers.length,
-                    a = true;
-                for (var i = 0; i < l; i++) {
-                    if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) {
-                        a = false;
-                    }
-                }
-                if (a) {
-                    parsers.push(parser);
-                };
-            };
-            this.addWidget = function (widget) {
-                widgets.push(widget);
-            };
-            this.formatFloat = function (s) {
-                var i = parseFloat(s);
-                return (isNaN(i)) ? 0 : i;
-            };
-            this.formatInt = function (s) {
-                var i = parseInt(s);
-                return (isNaN(i)) ? 0 : i;
-            };
-            this.isDigit = function (s, config) {
-                // replace all an wanted chars and match.
-                return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, '')));
-            };
-            this.clearTableBody = function (table) {
-                if ($.browser.msie) {
-                    function empty() {
-                        while (this.firstChild)
-                        this.removeChild(this.firstChild);
-                    }
-                    empty.apply(table.tBodies[0]);
-                } else {
-                    table.tBodies[0].innerHTML = "";
-                }
-            };
-        }
-    });
-
-    // extend plugin scope
-    $.fn.extend({
-        tablesorter: $.tablesorter.construct
-    });
-
-    // make shortcut
-    var ts = $.tablesorter;
-
-    // add default parsers
-    ts.addParser({
-        id: "text",
-        is: function (s) {
-            return true;
-        }, format: function (s) {
-            return $.trim(s.toLocaleLowerCase());
-        }, type: "text"
-    });
-
-    ts.addParser({
-        id: "digit",
-        is: function (s, table) {
-            var c = table.config;
-            return $.tablesorter.isDigit(s, c);
-        }, format: function (s) {
-            return $.tablesorter.formatFloat(s);
-        }, type: "numeric"
-    });
-
-    ts.addParser({
-        id: "currency",
-        is: function (s) {
-            return /^[£$€?.]/.test(s);
-        }, format: function (s) {
-            return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), ""));
-        }, type: "numeric"
-    });
-
-    ts.addParser({
-        id: "ipAddress",
-        is: function (s) {
-            return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);
-        }, format: function (s) {
-            var a = s.split("."),
-                r = "",
-                l = a.length;
-            for (var i = 0; i < l; i++) {
-                var item = a[i];
-                if (item.length == 2) {
-                    r += "0" + item;
-                } else {
-                    r += item;
-                }
-            }
-            return $.tablesorter.formatFloat(r);
-        }, type: "numeric"
-    });
-
-    ts.addParser({
-        id: "url",
-        is: function (s) {
-            return /^(https?|ftp|file):\/\/$/.test(s);
-        }, format: function (s) {
-            return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), ''));
-        }, type: "text"
-    });
-
-    ts.addParser({
-        id: "isoDate",
-        is: function (s) {
-            return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);
-        }, format: function (s) {
-            return $.tablesorter.formatFloat((s != "") ? new Date(s.replace(
-            new RegExp(/-/g), "/")).getTime() : "0");
-        }, type: "numeric"
-    });
-
-    ts.addParser({
-        id: "percent",
-        is: function (s) {
-            return /\%$/.test($.trim(s));
-        }, format: function (s) {
-            return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), ""));
-        }, type: "numeric"
-    });
-
-    ts.addParser({
-        id: "usLongDate",
-        is: function (s) {
-            return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));
-        }, format: function (s) {
-            return $.tablesorter.formatFloat(new Date(s).getTime());
-        }, type: "numeric"
-    });
-
-    ts.addParser({
-        id: "shortDate",
-        is: function (s) {
-            return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);
-        }, format: function (s, table) {
-            var c = table.config;
-            s = s.replace(/\-/g, "/");
-            if (c.dateFormat == "us") {
-                // reformat the string in ISO format
-                s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2");
-            } else if (c.dateFormat == "uk") {
-                // reformat the string in ISO format
-                s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1");
-            } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") {
-                s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3");
-            }
-            return $.tablesorter.formatFloat(new Date(s).getTime());
-        }, type: "numeric"
-    });
-    ts.addParser({
-        id: "time",
-        is: function (s) {
-            return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);
-        }, format: function (s) {
-            return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime());
-        }, type: "numeric"
-    });
-    ts.addParser({
-        id: "metadata",
-        is: function (s) {
-            return false;
-        }, format: function (s, table, cell) {
-            var c = table.config,
-                p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
-            return $(cell).metadata()[p];
-        }, type: "numeric"
-    });
-    // add default widgets
-    ts.addWidget({
-        id: "zebra",
-        format: function (table) {
-            if (table.config.debug) {
-                var time = new Date();
-            }
-            var $tr, row = -1,
-                odd;
-            // loop through the visible rows
-            $("tr:visible", table.tBodies[0]).each(function (i) {
-                $tr = $(this);
-                // style children rows the same way the parent
-                // row was styled
-                if (!$tr.hasClass(table.config.cssChildRow)) row++;
-                odd = (row % 2 == 0);
-                $tr.removeClass(
-                table.config.widgetZebra.css[odd ? 0 : 1]).addClass(
-                table.config.widgetZebra.css[odd ? 1 : 0])
-            });
-            if (table.config.debug) {
-                $.tablesorter.benchmark("Applying Zebra widget", time);
-            }
-        }
-    });
-})(jQuery);
\ No newline at end of file
+(function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery){
+/*! TableSorter (FORK) v2.31.3 *//*
+* Client-side table sorting with ease!
+* @requires jQuery v1.2.6+
+*
+* Copyright (c) 2007 Christian Bach
+* fork maintained by Rob Garrison
+*
+* Examples and original docs at: http://tablesorter.com
+* Dual licensed under the MIT and GPL licenses:
+* http://www.opensource.org/licenses/mit-license.php
+* http://www.gnu.org/licenses/gpl.html
+*
+* @type jQuery
+* @name tablesorter (FORK)
+* @cat Plugins/Tablesorter
+* @author Christian Bach - christian.bach@polyester.se
+* @contributor Rob Garrison - https://github.com/Mottie/tablesorter
+* @docs (fork) - https://mottie.github.io/tablesorter/docs/
+*/
+/*jshint browser:true, jquery:true, unused:false, expr: true */
+;( function( $ ) {
+	'use strict';
+	var ts = $.tablesorter = {
+
+		version : '2.31.3',
+
+		parsers : [],
+		widgets : [],
+		defaults : {
+
+			// *** appearance
+			theme            : 'default',  // adds tablesorter-{theme} to the table for styling
+			widthFixed       : false,      // adds colgroup to fix widths of columns
+			showProcessing   : false,      // show an indeterminate timer icon in the header when the table is sorted or filtered.
+
+			headerTemplate   : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> // class from cssIcon
+			onRenderTemplate : null,       // function( index, template ) { return template; }, // template is a string
+			onRenderHeader   : null,       // function( index ) {}, // nothing to return
+
+			// *** functionality
+			cancelSelection  : true,       // prevent text selection in the header
+			tabIndex         : true,       // add tabindex to header for keyboard accessibility
+			dateFormat       : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd'
+			sortMultiSortKey : 'shiftKey', // key used to select additional columns
+			sortResetKey     : 'ctrlKey',  // key used to remove sorting on a column
+			usNumberFormat   : true,       // false for German '1.234.567,89' or French '1 234 567,89'
+			delayInit        : false,      // if false, the parsed table contents will not update until the first sort
+			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.
+			resort           : true,       // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed
+
+			// *** sort options
+			headers          : {},         // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
+			ignoreCase       : true,       // ignore case while sorting
+			sortForce        : null,       // column(s) first sorted; always applied
+			sortList         : [],         // Initial sort order; applied initially; updated when manually sorted
+			sortAppend       : null,       // column(s) sorted last; always applied
+			sortStable       : false,      // when sorting two rows with exactly the same content, the original sort order is maintained
+
+			sortInitialOrder : 'asc',      // sort direction on first click
+			sortLocaleCompare: false,      // replace equivalent character (accented characters)
+			sortReset        : false,      // third click on the header will reset column to default - unsorted
+			sortRestart      : false,      // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns
+
+			emptyTo          : 'bottom',   // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin
+			stringTo         : 'max',      // sort strings in numerical column as max, min, top, bottom, zero
+			duplicateSpan    : true,       // colspan cells in the tbody will have duplicated content in the cache for each spanned column
+			textExtraction   : 'basic',    // text extraction method/function - function( node, table, cellIndex ) {}
+			textAttribute    : 'data-text',// data-attribute that contains alternate cell text (used in default textExtraction function)
+			textSorter       : null,       // choose overall or specific column sorter function( a, b, direction, table, columnIndex ) [alt: ts.sortText]
+			numberSorter     : null,       // choose overall numeric sorter function( a, b, direction, maxColumnValue )
+
+			// *** widget options
+			initWidgets      : true,       // apply widgets on tablesorter initialization
+			widgetClass      : 'widget-{name}', // table class name template to match to include a widget
+			widgets          : [],         // method to add widgets, e.g. widgets: ['zebra']
+			widgetOptions    : {
+				zebra : [ 'even', 'odd' ]  // zebra widget alternating row class names
+			},
+
+			// *** callbacks
+			initialized      : null,       // function( table ) {},
+
+			// *** extra css class names
+			tableClass       : '',
+			cssAsc           : '',
+			cssDesc          : '',
+			cssNone          : '',
+			cssHeader        : '',
+			cssHeaderRow     : '',
+			cssProcessing    : '', // processing icon applied to header during sort/filter
+
+			cssChildRow      : 'tablesorter-childRow', // class name indiciating that a row is to be attached to its parent
+			cssInfoBlock     : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
+			cssNoSort        : 'tablesorter-noSort',   // class name added to element inside header; clicking on it won't cause a sort
+			cssIgnoreRow     : 'tablesorter-ignoreRow',// header row to ignore; cells within this row will not be added to c.$headers
+
+			cssIcon          : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate
+			cssIconNone      : '', // class name added to the icon when there is no column sort
+			cssIconAsc       : '', // class name added to the icon when the column has an ascending sort
+			cssIconDesc      : '', // class name added to the icon when the column has a descending sort
+			cssIconDisabled  : '', // class name added to the icon when the column has a disabled sort
+
+			// *** events
+			pointerClick     : 'click',
+			pointerDown      : 'mousedown',
+			pointerUp        : 'mouseup',
+
+			// *** selectors
+			selectorHeaders  : '> thead th, > thead td',
+			selectorSort     : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
+			selectorRemove   : '.remove-me',
+
+			// *** advanced
+			debug            : false,
+
+			// *** Internal variables
+			headerList: [],
+			empties: {},
+			strings: {},
+			parsers: [],
+
+			// *** parser options for validator; values must be falsy!
+			globalize: 0,
+			imgAttr: 0
+
+			// removed: widgetZebra: { css: ['even', 'odd'] }
+
+		},
+
+		// internal css classes - these will ALWAYS be added to
+		// the table and MUST only contain one class name - fixes #381
+		css : {
+			table      : 'tablesorter',
+			cssHasChild: 'tablesorter-hasChildRow',
+			childRow   : 'tablesorter-childRow',
+			colgroup   : 'tablesorter-colgroup',
+			header     : 'tablesorter-header',
+			headerRow  : 'tablesorter-headerRow',
+			headerIn   : 'tablesorter-header-inner',
+			icon       : 'tablesorter-icon',
+			processing : 'tablesorter-processing',
+			sortAsc    : 'tablesorter-headerAsc',
+			sortDesc   : 'tablesorter-headerDesc',
+			sortNone   : 'tablesorter-headerUnSorted'
+		},
+
+		// labels applied to sortable headers for accessibility (aria) support
+		language : {
+			sortAsc      : 'Ascending sort applied, ',
+			sortDesc     : 'Descending sort applied, ',
+			sortNone     : 'No sort applied, ',
+			sortDisabled : 'sorting is disabled',
+			nextAsc      : 'activate to apply an ascending sort',
+			nextDesc     : 'activate to apply a descending sort',
+			nextNone     : 'activate to remove the sort'
+		},
+
+		regex : {
+			templateContent : /\{content\}/g,
+			templateIcon    : /\{icon\}/g,
+			templateName    : /\{name\}/i,
+			spaces          : /\s+/g,
+			nonWord         : /\W/g,
+			formElements    : /(input|select|button|textarea)/i,
+
+			// *** sort functions ***
+			// regex used in natural sort
+			// chunk/tokenize numbers & letters
+			chunk  : /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,
+			// replace chunks @ ends
+			chunks : /(^\\0|\\0$)/,
+			hex    : /^0x[0-9a-f]+$/i,
+
+			// *** formatFloat ***
+			comma                : /,/g,
+			digitNonUS           : /[\s|\.]/g,
+			digitNegativeTest    : /^\s*\([.\d]+\)/,
+			digitNegativeReplace : /^\s*\(([.\d]+)\)/,
+
+			// *** isDigit ***
+			digitTest    : /^[\-+(]?\d+[)]?$/,
+			digitReplace : /[,.'"\s]/g
+
+		},
+
+		// digit sort, text location
+		string : {
+			max      : 1,
+			min      : -1,
+			emptymin : 1,
+			emptymax : -1,
+			zero     : 0,
+			none     : 0,
+			'null'   : 0,
+			top      : true,
+			bottom   : false
+		},
+
+		keyCodes : {
+			enter : 13
+		},
+
+		// placeholder date parser data (globalize)
+		dates : {},
+
+		// These methods can be applied on table.config instance
+		instanceMethods : {},
+
+		/*
+		▄█████ ██████ ██████ ██  ██ █████▄
+		▀█▄    ██▄▄     ██   ██  ██ ██▄▄██
+		   ▀█▄ ██▀▀     ██   ██  ██ ██▀▀▀
+		█████▀ ██████   ██   ▀████▀ ██
+		*/
+
+		setup : function( table, c ) {
+			// if no thead or tbody, or tablesorter is already present, quit
+			if ( !table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true ) {
+				if ( ts.debug(c, 'core') ) {
+					if ( table.hasInitialized ) {
+						console.warn( 'Stopping initialization. Tablesorter has already been initialized' );
+					} else {
+						console.error( 'Stopping initialization! No table, thead or tbody', table );
+					}
+				}
+				return;
+			}
+
+			var tmp = '',
+				$table = $( table ),
+				meta = $.metadata;
+			// initialization flag
+			table.hasInitialized = false;
+			// table is being processed flag
+			table.isProcessing = true;
+			// make sure to store the config object
+			table.config = c;
+			// save the settings where they read
+			$.data( table, 'tablesorter', c );
+			if ( ts.debug(c, 'core') ) {
+				console[ console.group ? 'group' : 'log' ]( 'Initializing tablesorter v' + ts.version );
+				$.data( table, 'startoveralltimer', new Date() );
+			}
+
+			// removing this in version 3 (only supports jQuery 1.7+)
+			c.supportsDataObject = ( function( version ) {
+				version[ 0 ] = parseInt( version[ 0 ], 10 );
+				return ( version[ 0 ] > 1 ) || ( version[ 0 ] === 1 && parseInt( version[ 1 ], 10 ) >= 4 );
+			})( $.fn.jquery.split( '.' ) );
+			// ensure case insensitivity
+			c.emptyTo = c.emptyTo.toLowerCase();
+			c.stringTo = c.stringTo.toLowerCase();
+			c.last = { sortList : [], clickedIndex : -1 };
+			// add table theme class only if there isn't already one there
+			if ( !/tablesorter\-/.test( $table.attr( 'class' ) ) ) {
+				tmp = ( c.theme !== '' ? ' tablesorter-' + c.theme : '' );
+			}
+
+			// give the table a unique id, which will be used in namespace binding
+			if ( !c.namespace ) {
+				c.namespace = '.tablesorter' + Math.random().toString( 16 ).slice( 2 );
+			} else {
+				// make sure namespace starts with a period & doesn't have weird characters
+				c.namespace = '.' + c.namespace.replace( ts.regex.nonWord, '' );
+			}
+
+			c.table = table;
+			c.$table = $table
+				// add namespace to table to allow bindings on extra elements to target
+				// the parent table (e.g. parser-input-select)
+				.addClass( ts.css.table + ' ' + c.tableClass + tmp + ' ' + c.namespace.slice(1) )
+				.attr( 'role', 'grid' );
+			c.$headers = $table.find( c.selectorHeaders );
+
+			c.$table.children().children( 'tr' ).attr( 'role', 'row' );
+			c.$tbodies = $table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ).attr({
+				'aria-live' : 'polite',
+				'aria-relevant' : 'all'
+			});
+			if ( c.$table.children( 'caption' ).length ) {
+				tmp = c.$table.children( 'caption' )[ 0 ];
+				if ( !tmp.id ) { tmp.id = c.namespace.slice( 1 ) + 'caption'; }
+				c.$table.attr( 'aria-labelledby', tmp.id );
+			}
+			c.widgetInit = {}; // keep a list of initialized widgets
+			// change textExtraction via data-attribute
+			c.textExtraction = c.$table.attr( 'data-text-extraction' ) || c.textExtraction || 'basic';
+			// build headers
+			ts.buildHeaders( c );
+			// fixate columns if the users supplies the fixedWidth option
+			// do this after theme has been applied
+			ts.fixColumnWidth( table );
+			// add widgets from class name
+			ts.addWidgetFromClass( table );
+			// add widget options before parsing (e.g. grouping widget has parser settings)
+			ts.applyWidgetOptions( table );
+			// try to auto detect column type, and store in tables config
+			ts.setupParsers( c );
+			// start total row count at zero
+			c.totalRows = 0;
+			// only validate options while debugging. See #1528
+			if (c.debug) {
+				ts.validateOptions( c );
+			}
+			// build the cache for the tbody cells
+			// delayInit will delay building the cache until the user starts a sort
+			if ( !c.delayInit ) { ts.buildCache( c ); }
+			// bind all header events and methods
+			ts.bindEvents( table, c.$headers, true );
+			ts.bindMethods( c );
+			// get sort list from jQuery data or metadata
+			// in jQuery < 1.4, an error occurs when calling $table.data()
+			if ( c.supportsDataObject && typeof $table.data().sortlist !== 'undefined' ) {
+				c.sortList = $table.data().sortlist;
+			} else if ( meta && ( $table.metadata() && $table.metadata().sortlist ) ) {
+				c.sortList = $table.metadata().sortlist;
+			}
+			// apply widget init code
+			ts.applyWidget( table, true );
+			// if user has supplied a sort list to constructor
+			if ( c.sortList.length > 0 ) {
+				// save sortList before any sortAppend is added
+				c.last.sortList = c.sortList;
+				ts.sortOn( c, c.sortList, {}, !c.initWidgets );
+			} else {
+				ts.setHeadersCss( c );
+				if ( c.initWidgets ) {
+					// apply widget format
+					ts.applyWidget( table, false );
+				}
+			}
+
+			// show processesing icon
+			if ( c.showProcessing ) {
+				$table
+				.unbind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace )
+				.bind( 'sortBegin' + c.namespace + ' sortEnd' + c.namespace, function( e ) {
+					clearTimeout( c.timerProcessing );
+					ts.isProcessing( table );
+					if ( e.type === 'sortBegin' ) {
+						c.timerProcessing = setTimeout( function() {
+							ts.isProcessing( table, true );
+						}, 500 );
+					}
+				});
+			}
+
+			// initialized
+			table.hasInitialized = true;
+			table.isProcessing = false;
+			if ( ts.debug(c, 'core') ) {
+				console.log( 'Overall initialization time:' + ts.benchmark( $.data( table, 'startoveralltimer' ) ) );
+				if ( ts.debug(c, 'core') && console.groupEnd ) { console.groupEnd(); }
+			}
+			$table.triggerHandler( 'tablesorter-initialized', table );
+			if ( typeof c.initialized === 'function' ) {
+				c.initialized( table );
+			}
+		},
+
+		bindMethods : function( c ) {
+			var $table = c.$table,
+				namespace = c.namespace,
+				events = ( 'sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' +
+					'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' +
+					'mouseleave ' ).split( ' ' )
+					.join( namespace + ' ' );
+			// apply easy methods that trigger bound events
+			$table
+			.unbind( events.replace( ts.regex.spaces, ' ' ) )
+			.bind( 'sortReset' + namespace, function( e, callback ) {
+				e.stopPropagation();
+				// using this.config to ensure functions are getting a non-cached version of the config
+				ts.sortReset( this.config, function( table ) {
+					if (table.isApplyingWidgets) {
+						// multiple triggers in a row... filterReset, then sortReset - see #1361
+						// wait to update widgets
+						setTimeout( function() {
+							ts.applyWidget( table, '', callback );
+						}, 100 );
+					} else {
+						ts.applyWidget( table, '', callback );
+					}
+				});
+			})
+			.bind( 'updateAll' + namespace, function( e, resort, callback ) {
+				e.stopPropagation();
+				ts.updateAll( this.config, resort, callback );
+			})
+			.bind( 'update' + namespace + ' updateRows' + namespace, function( e, resort, callback ) {
+				e.stopPropagation();
+				ts.update( this.config, resort, callback );
+			})
+			.bind( 'updateHeaders' + namespace, function( e, callback ) {
+				e.stopPropagation();
+				ts.updateHeaders( this.config, callback );
+			})
+			.bind( 'updateCell' + namespace, function( e, cell, resort, callback ) {
+				e.stopPropagation();
+				ts.updateCell( this.config, cell, resort, callback );
+			})
+			.bind( 'addRows' + namespace, function( e, $row, resort, callback ) {
+				e.stopPropagation();
+				ts.addRows( this.config, $row, resort, callback );
+			})
+			.bind( 'updateComplete' + namespace, function() {
+				this.isUpdating = false;
+			})
+			.bind( 'sorton' + namespace, function( e, list, callback, init ) {
+				e.stopPropagation();
+				ts.sortOn( this.config, list, callback, init );
+			})
+			.bind( 'appendCache' + namespace, function( e, callback, init ) {
+				e.stopPropagation();
+				ts.appendCache( this.config, init );
+				if ( $.isFunction( callback ) ) {
+					callback( this );
+				}
+			})
+			// $tbodies variable is used by the tbody sorting widget
+			.bind( 'updateCache' + namespace, function( e, callback, $tbodies ) {
+				e.stopPropagation();
+				ts.updateCache( this.config, callback, $tbodies );
+			})
+			.bind( 'applyWidgetId' + namespace, function( e, id ) {
+				e.stopPropagation();
+				ts.applyWidgetId( this, id );
+			})
+			.bind( 'applyWidgets' + namespace, function( e, callback ) {
+				e.stopPropagation();
+				// apply widgets (false = not initializing)
+				ts.applyWidget( this, false, callback );
+			})
+			.bind( 'refreshWidgets' + namespace, function( e, all, dontapply ) {
+				e.stopPropagation();
+				ts.refreshWidgets( this, all, dontapply );
+			})
+			.bind( 'removeWidget' + namespace, function( e, name, refreshing ) {
+				e.stopPropagation();
+				ts.removeWidget( this, name, refreshing );
+			})
+			.bind( 'destroy' + namespace, function( e, removeClasses, callback ) {
+				e.stopPropagation();
+				ts.destroy( this, removeClasses, callback );
+			})
+			.bind( 'resetToLoadState' + namespace, function( e ) {
+				e.stopPropagation();
+				// remove all widgets
+				ts.removeWidget( this, true, false );
+				var tmp = $.extend( true, {}, c.originalSettings );
+				// restore original settings; this clears out current settings, but does not clear
+				// values saved to storage.
+				c = $.extend( true, {}, ts.defaults, tmp );
+				c.originalSettings = tmp;
+				this.hasInitialized = false;
+				// setup the entire table again
+				ts.setup( this, c );
+			});
+		},
+
+		bindEvents : function( table, $headers, core ) {
+			table = $( table )[ 0 ];
+			var tmp,
+				c = table.config,
+				namespace = c.namespace,
+				downTarget = null;
+			if ( core !== true ) {
+				$headers.addClass( namespace.slice( 1 ) + '_extra_headers' );
+				tmp = ts.getClosest( $headers, 'table' );
+				if ( tmp.length && tmp[ 0 ].nodeName === 'TABLE' && tmp[ 0 ] !== table ) {
+					$( tmp[ 0 ] ).addClass( namespace.slice( 1 ) + '_extra_table' );
+				}
+			}
+			tmp = ( c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ' )
+				.replace( ts.regex.spaces, ' ' )
+				.split( ' ' )
+				.join( namespace + ' ' );
+			// apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
+			$headers
+			// http://stackoverflow.com/questions/5312849/jquery-find-self;
+			.find( c.selectorSort )
+			.add( $headers.filter( c.selectorSort ) )
+			.unbind( tmp )
+			.bind( tmp, function( e, external ) {
+				var $cell, cell, temp,
+					$target = $( e.target ),
+					// wrap event type in spaces, so the match doesn't trigger on inner words
+					type = ' ' + e.type + ' ';
+				// only recognize left clicks
+				if ( ( ( e.which || e.button ) !== 1 && !type.match( ' ' + c.pointerClick + ' | sort | keyup ' ) ) ||
+					// allow pressing enter
+					( type === ' keyup ' && e.which !== ts.keyCodes.enter ) ||
+					// allow triggering a click event (e.which is undefined) & ignore physical clicks
+					( type.match( ' ' + c.pointerClick + ' ' ) && typeof e.which !== 'undefined' ) ) {
+					return;
+				}
+				// ignore mouseup if mousedown wasn't on the same target
+				if ( type.match( ' ' + c.pointerUp + ' ' ) && downTarget !== e.target && external !== true ) {
+					return;
+				}
+				// set target on mousedown
+				if ( type.match( ' ' + c.pointerDown + ' ' ) ) {
+					downTarget = e.target;
+					// preventDefault needed or jQuery v1.3.2 and older throws an
+					// "Uncaught TypeError: handler.apply is not a function" error
+					temp = $target.jquery.split( '.' );
+					if ( temp[ 0 ] === '1' && temp[ 1 ] < 4 ) { e.preventDefault(); }
+					return;
+				}
+				downTarget = null;
+				$cell = ts.getClosest( $( this ), '.' + ts.css.header );
+				// prevent sort being triggered on form elements
+				if ( ts.regex.formElements.test( e.target.nodeName ) ||
+					// nosort class name, or elements within a nosort container
+					$target.hasClass( c.cssNoSort ) || $target.parents( '.' + c.cssNoSort ).length > 0 ||
+					// disabled cell directly clicked
+					$cell.hasClass( 'sorter-false' ) ||
+					// elements within a button
+					$target.parents( 'button' ).length > 0 ) {
+					return !c.cancelSelection;
+				}
+				if ( c.delayInit && ts.isEmptyObject( c.cache ) ) {
+					ts.buildCache( c );
+				}
+				// use column index from data-attribute or index of current row; fixes #1116
+				c.last.clickedIndex = $cell.attr( 'data-column' ) || $cell.index();
+				cell = c.$headerIndexed[ c.last.clickedIndex ][0];
+				if ( cell && !cell.sortDisabled ) {
+					ts.initSort( c, cell, e );
+				}
+			});
+			if ( c.cancelSelection ) {
+				// cancel selection
+				$headers
+					.attr( 'unselectable', 'on' )
+					.bind( 'selectstart', false )
+					.css({
+						'user-select' : 'none',
+						'MozUserSelect' : 'none' // not needed for jQuery 1.8+
+					});
+			}
+		},
+
+		buildHeaders : function( c ) {
+			var $temp, icon, timer, indx;
+			c.headerList = [];
+			c.headerContent = [];
+			c.sortVars = [];
+			if ( ts.debug(c, 'core') ) {
+				timer = new Date();
+			}
+			// children tr in tfoot - see issue #196 & #547
+			// don't pass table.config to computeColumnIndex here - widgets (math) pass it to "quickly" index tbody cells
+			c.columns = ts.computeColumnIndex( c.$table.children( 'thead, tfoot' ).children( 'tr' ) );
+			// add icon if cssIcon option exists
+			icon = c.cssIcon ?
+				'<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' :
+				'';
+			// redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
+			c.$headers = $( $.map( c.$table.find( c.selectorHeaders ), function( elem, index ) {
+				var configHeaders, header, column, template, tmp,
+					$elem = $( elem );
+				// ignore cell (don't add it to c.$headers) if row has ignoreRow class
+				if ( ts.getClosest( $elem, 'tr' ).hasClass( c.cssIgnoreRow ) ) { return; }
+				// transfer data-column to element if not th/td - #1459
+				if ( !/(th|td)/i.test( elem.nodeName ) ) {
+					tmp = ts.getClosest( $elem, 'th, td' );
+					$elem.attr( 'data-column', tmp.attr( 'data-column' ) );
+				}
+				// make sure to get header cell & not column indexed cell
+				configHeaders = ts.getColumnData( c.table, c.headers, index, true );
+				// save original header content
+				c.headerContent[ index ] = $elem.html();
+				// if headerTemplate is empty, don't reformat the header cell
+				if ( c.headerTemplate !== '' && !$elem.find( '.' + ts.css.headerIn ).length ) {
+					// set up header template
+					template = c.headerTemplate
+						.replace( ts.regex.templateContent, $elem.html() )
+						.replace( ts.regex.templateIcon, $elem.find( '.' + ts.css.icon ).length ? '' : icon );
+					if ( c.onRenderTemplate ) {
+						header = c.onRenderTemplate.apply( $elem, [ index, template ] );
+						// only change t if something is returned
+						if ( header && typeof header === 'string' ) {
+							template = header;
+						}
+					}
+					$elem.html( '<div class="' + ts.css.headerIn + '">' + template + '</div>' ); // faster than wrapInner
+				}
+				if ( c.onRenderHeader ) {
+					c.onRenderHeader.apply( $elem, [ index, c, c.$table ] );
+				}
+				column = parseInt( $elem.attr( 'data-column' ), 10 );
+				elem.column = column;
+				tmp = ts.getOrder( ts.getData( $elem, configHeaders, 'sortInitialOrder' ) || c.sortInitialOrder );
+				// this may get updated numerous times if there are multiple rows
+				c.sortVars[ column ] = {
+					count : -1, // set to -1 because clicking on the header automatically adds one
+					order : tmp ?
+						( c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ] ) : // desc, asc, unsorted
+						( c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ] ),  // asc, desc, unsorted
+					lockedOrder : false,
+					sortedBy : ''
+				};
+				tmp = ts.getData( $elem, configHeaders, 'lockedOrder' ) || false;
+				if ( typeof tmp !== 'undefined' && tmp !== false ) {
+					c.sortVars[ column ].lockedOrder = true;
+					c.sortVars[ column ].order = ts.getOrder( tmp ) ? [ 1, 1 ] : [ 0, 0 ];
+				}
+				// add cell to headerList
+				c.headerList[ index ] = elem;
+				$elem.addClass( ts.css.header + ' ' + c.cssHeader );
+				// add to parent in case there are multiple rows
+				ts.getClosest( $elem, 'tr' )
+					.addClass( ts.css.headerRow + ' ' + c.cssHeaderRow )
+					.attr( 'role', 'row' );
+				// allow keyboard cursor to focus on element
+				if ( c.tabIndex ) {
+					$elem.attr( 'tabindex', 0 );
+				}
+				return elem;
+			}) );
+			// cache headers per column
+			c.$headerIndexed = [];
+			for ( indx = 0; indx < c.columns; indx++ ) {
+				// colspan in header making a column undefined
+				if ( ts.isEmptyObject( c.sortVars[ indx ] ) ) {
+					c.sortVars[ indx ] = {};
+				}
+				// Use c.$headers.parent() in case selectorHeaders doesn't point to the th/td
+				$temp = c.$headers.filter( '[data-column="' + indx + '"]' );
+				// target sortable column cells, unless there are none, then use non-sortable cells
+				// .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6
+				c.$headerIndexed[ indx ] = $temp.length ?
+					$temp.not( '.sorter-false' ).length ?
+						$temp.not( '.sorter-false' ).filter( ':last' ) :
+						$temp.filter( ':last' ) :
+					$();
+			}
+			c.$table.find( c.selectorHeaders ).attr({
+				scope: 'col',
+				role : 'columnheader'
+			});
+			// enable/disable sorting
+			ts.updateHeader( c );
+			if ( ts.debug(c, 'core') ) {
+				console.log( 'Built headers:' + ts.benchmark( timer ) );
+				console.log( c.$headers );
+			}
+		},
+
+		// Use it to add a set of methods to table.config which will be available for all tables.
+		// This should be done before table initialization
+		addInstanceMethods : function( methods ) {
+			$.extend( ts.instanceMethods, methods );
+		},
+
+		/*
+		█████▄ ▄████▄ █████▄ ▄█████ ██████ █████▄ ▄█████
+		██▄▄██ ██▄▄██ ██▄▄██ ▀█▄    ██▄▄   ██▄▄██ ▀█▄
+		██▀▀▀  ██▀▀██ ██▀██     ▀█▄ ██▀▀   ██▀██     ▀█▄
+		██     ██  ██ ██  ██ █████▀ ██████ ██  ██ █████▀
+		*/
+		setupParsers : function( c, $tbodies ) {
+			var rows, list, span, max, colIndex, indx, header, configHeaders,
+				noParser, parser, extractor, time, tbody, len,
+				table = c.table,
+				tbodyIndex = 0,
+				debug = ts.debug(c, 'core'),
+				debugOutput = {};
+			// update table bodies in case we start with an empty table
+			c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
+			tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies;
+			len = tbody.length;
+			if ( len === 0 ) {
+				return debug ? console.warn( 'Warning: *Empty table!* Not building a parser cache' ) : '';
+			} else if ( debug ) {
+				time = new Date();
+				console[ console.group ? 'group' : 'log' ]( 'Detecting parsers for each column' );
+			}
+			list = {
+				extractors: [],
+				parsers: []
+			};
+			while ( tbodyIndex < len ) {
+				rows = tbody[ tbodyIndex ].rows;
+				if ( rows.length ) {
+					colIndex = 0;
+					max = c.columns;
+					for ( indx = 0; indx < max; indx++ ) {
+						header = c.$headerIndexed[ colIndex ];
+						if ( header && header.length ) {
+							// get column indexed table cell; adding true parameter fixes #1362 but
+							// it would break backwards compatibility...
+							configHeaders = ts.getColumnData( table, c.headers, colIndex ); // , true );
+							// get column parser/extractor
+							extractor = ts.getParserById( ts.getData( header, configHeaders, 'extractor' ) );
+							parser = ts.getParserById( ts.getData( header, configHeaders, 'sorter' ) );
+							noParser = ts.getData( header, configHeaders, 'parser' ) === 'false';
+							// empty cells behaviour - keeping emptyToBottom for backwards compatibility
+							c.empties[colIndex] = (
+								ts.getData( header, configHeaders, 'empty' ) ||
+								c.emptyTo || ( c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase();
+							// text strings behaviour in numerical sorts
+							c.strings[colIndex] = (
+								ts.getData( header, configHeaders, 'string' ) ||
+								c.stringTo ||
+								'max' ).toLowerCase();
+							if ( noParser ) {
+								parser = ts.getParserById( 'no-parser' );
+							}
+							if ( !extractor ) {
+								// For now, maybe detect someday
+								extractor = false;
+							}
+							if ( !parser ) {
+								parser = ts.detectParserForColumn( c, rows, -1, colIndex );
+							}
+							if ( debug ) {
+								debugOutput[ '(' + colIndex + ') ' + header.text() ] = {
+									parser : parser.id,
+									extractor : extractor ? extractor.id : 'none',
+									string : c.strings[ colIndex ],
+									empty  : c.empties[ colIndex ]
+								};
+							}
+							list.parsers[ colIndex ] = parser;
+							list.extractors[ colIndex ] = extractor;
+							span = header[ 0 ].colSpan - 1;
+							if ( span > 0 ) {
+								colIndex += span;
+								max += span;
+								while ( span + 1 > 0 ) {
+									// set colspan columns to use the same parsers & extractors
+									list.parsers[ colIndex - span ] = parser;
+									list.extractors[ colIndex - span ] = extractor;
+									span--;
+								}
+							}
+						}
+						colIndex++;
+					}
+				}
+				tbodyIndex += ( list.parsers.length ) ? len : 1;
+			}
+			if ( debug ) {
+				if ( !ts.isEmptyObject( debugOutput ) ) {
+					console[ console.table ? 'table' : 'log' ]( debugOutput );
+				} else {
+					console.warn( '  No parsers detected!' );
+				}
+				console.log( 'Completed detecting parsers' + ts.benchmark( time ) );
+				if ( console.groupEnd ) { console.groupEnd(); }
+			}
+			c.parsers = list.parsers;
+			c.extractors = list.extractors;
+		},
+
+		addParser : function( parser ) {
+			var indx,
+				len = ts.parsers.length,
+				add = true;
+			for ( indx = 0; indx < len; indx++ ) {
+				if ( ts.parsers[ indx ].id.toLowerCase() === parser.id.toLowerCase() ) {
+					add = false;
+				}
+			}
+			if ( add ) {
+				ts.parsers[ ts.parsers.length ] = parser;
+			}
+		},
+
+		getParserById : function( name ) {
+			/*jshint eqeqeq:false */ // eslint-disable-next-line eqeqeq
+			if ( name == 'false' ) { return false; }
+			var indx,
+				len = ts.parsers.length;
+			for ( indx = 0; indx < len; indx++ ) {
+				if ( ts.parsers[ indx ].id.toLowerCase() === ( name.toString() ).toLowerCase() ) {
+					return ts.parsers[ indx ];
+				}
+			}
+			return false;
+		},
+
+		detectParserForColumn : function( c, rows, rowIndex, cellIndex ) {
+			var cur, $node, row,
+				indx = ts.parsers.length,
+				node = false,
+				nodeValue = '',
+				debug = ts.debug(c, 'core'),
+				keepLooking = true;
+			while ( nodeValue === '' && keepLooking ) {
+				rowIndex++;
+				row = rows[ rowIndex ];
+				// stop looking after 50 empty rows
+				if ( row && rowIndex < 50 ) {
+					if ( row.className.indexOf( ts.cssIgnoreRow ) < 0 ) {
+						node = rows[ rowIndex ].cells[ cellIndex ];
+						nodeValue = ts.getElementText( c, node, cellIndex );
+						$node = $( node );
+						if ( debug ) {
+							console.log( 'Checking if value was empty on row ' + rowIndex + ', column: ' +
+								cellIndex + ': "' + nodeValue + '"' );
+						}
+					}
+				} else {
+					keepLooking = false;
+				}
+			}
+			while ( --indx >= 0 ) {
+				cur = ts.parsers[ indx ];
+				// ignore the default text parser because it will always be true
+				if ( cur && cur.id !== 'text' && cur.is && cur.is( nodeValue, c.table, node, $node ) ) {
+					return cur;
+				}
+			}
+			// nothing found, return the generic parser (text)
+			return ts.getParserById( 'text' );
+		},
+
+		getElementText : function( c, node, cellIndex ) {
+			if ( !node ) { return ''; }
+			var tmp,
+				extract = c.textExtraction || '',
+				// node could be a jquery object
+				// http://jsperf.com/jquery-vs-instanceof-jquery/2
+				$node = node.jquery ? node : $( node );
+			if ( typeof extract === 'string' ) {
+				// check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
+				// http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/
+				if ( extract === 'basic' && typeof ( tmp = $node.attr( c.textAttribute ) ) !== 'undefined' ) {
+					return $.trim( tmp );
+				}
+				return $.trim( node.textContent || $node.text() );
+			} else {
+				if ( typeof extract === 'function' ) {
+					return $.trim( extract( $node[ 0 ], c.table, cellIndex ) );
+				} else if ( typeof ( tmp = ts.getColumnData( c.table, extract, cellIndex ) ) === 'function' ) {
+					return $.trim( tmp( $node[ 0 ], c.table, cellIndex ) );
+				}
+			}
+			// fallback
+			return $.trim( $node[ 0 ].textContent || $node.text() );
+		},
+
+		// centralized function to extract/parse cell contents
+		getParsedText : function( c, cell, colIndex, txt ) {
+			if ( typeof txt === 'undefined' ) {
+				txt = ts.getElementText( c, cell, colIndex );
+			}
+			// if no parser, make sure to return the txt
+			var val = '' + txt,
+				parser = c.parsers[ colIndex ],
+				extractor = c.extractors[ colIndex ];
+			if ( parser ) {
+				// do extract before parsing, if there is one
+				if ( extractor && typeof extractor.format === 'function' ) {
+					txt = extractor.format( txt, c.table, cell, colIndex );
+				}
+				// allow parsing if the string is empty, previously parsing would change it to zero,
+				// in case the parser needs to extract data from the table cell attributes
+				val = parser.id === 'no-parser' ? '' :
+					// make sure txt is a string (extractor may have converted it)
+					parser.format( '' + txt, c.table, cell, colIndex );
+				if ( c.ignoreCase && typeof val === 'string' ) {
+					val = val.toLowerCase();
+				}
+			}
+			return val;
+		},
+
+		/*
+		▄████▄ ▄████▄ ▄████▄ ██  ██ ██████
+		██  ▀▀ ██▄▄██ ██  ▀▀ ██▄▄██ ██▄▄
+		██  ▄▄ ██▀▀██ ██  ▄▄ ██▀▀██ ██▀▀
+		▀████▀ ██  ██ ▀████▀ ██  ██ ██████
+		*/
+		buildCache : function( c, callback, $tbodies ) {
+			var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row,
+				cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData,
+				colMax, span, cacheIndex, hasParser, max, len, index,
+				table = c.table,
+				parsers = c.parsers,
+				debug = ts.debug(c, 'core');
+			// update tbody variable
+			c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' );
+			$tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
+			c.cache = {};
+			c.totalRows = 0;
+			// if no parsers found, return - it's an empty table.
+			if ( !parsers ) {
+				return debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : '';
+			}
+			if ( debug ) {
+				cacheTime = new Date();
+			}
+			// processing icon
+			if ( c.showProcessing ) {
+				ts.isProcessing( table, true );
+			}
+			for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) {
+				colMax = []; // column max value per tbody
+				cache = c.cache[ tbodyIndex ] = {
+					normalized: [] // array of normalized row data; last entry contains 'rowData' above
+					// colMax: #   // added at the end
+				};
+
+				totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0;
+				for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) {
+					rowData = {
+						// order: original row order #
+						// $row : jQuery Object[]
+						child: [], // child row text (filter widget)
+						raw: []    // original row text
+					};
+					/** Add the table data to main data array */
+					$row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] );
+					cols = [];
+					// ignore "remove-me" rows
+					if ( $row.hasClass( c.selectorRemove.slice(1) ) ) {
+						continue;
+					}
+					// if this is a child row, add it to the last row's children and continue to the next row
+					// ignore child row class, if it is the first row
+					if ( $row.hasClass( c.cssChildRow ) && rowIndex !== 0 ) {
+						len = cache.normalized.length - 1;
+						prevRowData = cache.normalized[ len ][ c.columns ];
+						prevRowData.$row = prevRowData.$row.add( $row );
+						// add 'hasChild' class name to parent row
+						if ( !$row.prev().hasClass( c.cssChildRow ) ) {
+							$row.prev().addClass( ts.css.cssHasChild );
+						}
+						// save child row content (un-parsed!)
+						$cells = $row.children( 'th, td' );
+						len = prevRowData.child.length;
+						prevRowData.child[ len ] = [];
+						// child row content does not account for colspans/rowspans; so indexing may be off
+						cacheIndex = 0;
+						max = c.columns;
+						for ( colIndex = 0; colIndex < max; colIndex++ ) {
+							cell = $cells[ colIndex ];
+							if ( cell ) {
+								prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex );
+								span = $cells[ colIndex ].colSpan - 1;
+								if ( span > 0 ) {
+									cacheIndex += span;
+									max += span;
+								}
+							}
+							cacheIndex++;
+						}
+						// go to the next for loop
+						continue;
+					}
+					rowData.$row = $row;
+					rowData.order = rowIndex; // add original row position to rowCache
+					cacheIndex = 0;
+					max = c.columns;
+					for ( colIndex = 0; colIndex < max; ++colIndex ) {
+						cell = $row[ 0 ].cells[ colIndex ];
+						if ( cell && cacheIndex < c.columns ) {
+							hasParser = typeof parsers[ cacheIndex ] !== 'undefined';
+							if ( !hasParser && debug ) {
+								console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex +
+									'; cell containing: "' + $(cell).text() + '"; does it have a header?' );
+							}
+							val = ts.getElementText( c, cell, cacheIndex );
+							rowData.raw[ cacheIndex ] = val; // save original row text
+							// save raw column text even if there is no parser set
+							txt = ts.getParsedText( c, cell, cacheIndex, val );
+							cols[ cacheIndex ] = txt;
+							if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
+								// determine column max value (ignore sign)
+								colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 );
+							}
+							// allow colSpan in tbody
+							span = cell.colSpan - 1;
+							if ( span > 0 ) {
+								index = 0;
+								while ( index <= span ) {
+									// duplicate text (or not) to spanned columns
+									// instead of setting duplicate span to empty string, use textExtraction to try to get a value
+									// see http://stackoverflow.com/q/36449711/145346
+									txt = c.duplicateSpan || index === 0 ?
+										txt :
+										typeof c.textExtraction !== 'string' ?
+											ts.getElementText( c, cell, cacheIndex + index ) || '' :
+											'';
+									rowData.raw[ cacheIndex + index ] = txt;
+									cols[ cacheIndex + index ] = txt;
+									index++;
+								}
+								cacheIndex += span;
+								max += span;
+							}
+						}
+						cacheIndex++;
+					}
+					// ensure rowData is always in the same location (after the last column)
+					cols[ c.columns ] = rowData;
+					cache.normalized[ cache.normalized.length ] = cols;
+				}
+				cache.colMax = colMax;
+				// total up rows, not including child rows
+				c.totalRows += cache.normalized.length;
+
+			}
+			if ( c.showProcessing ) {
+				ts.isProcessing( table ); // remove processing icon
+			}
+			if ( debug ) {
+				len = Math.min( 5, c.cache[ 0 ].normalized.length );
+				console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows +
+					' rows (showing ' + len + ' rows in log) and ' + c.columns + ' columns' +
+					ts.benchmark( cacheTime ) );
+				val = {};
+				for ( colIndex = 0; colIndex < c.columns; colIndex++ ) {
+					for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) {
+						if ( !val[ 'row: ' + cacheIndex ] ) {
+							val[ 'row: ' + cacheIndex ] = {};
+						}
+						val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] =
+							c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ];
+					}
+				}
+				console[ console.table ? 'table' : 'log' ]( val );
+				if ( console.groupEnd ) { console.groupEnd(); }
+			}
+			if ( $.isFunction( callback ) ) {
+				callback( table );
+			}
+		},
+
+		getColumnText : function( table, column, callback, rowFilter ) {
+			table = $( table )[0];
+			var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result,
+				hasCallback = typeof callback === 'function',
+				allColumns = column === 'all',
+				data = { raw : [], parsed: [], $cell: [] },
+				c = table.config;
+			if ( ts.isEmptyObject( c ) ) {
+				if ( ts.debug(c, 'core') ) {
+					console.warn( 'No cache found - aborting getColumnText function!' );
+				}
+			} else {
+				tbodyLen = c.$tbodies.length;
+				for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) {
+					cache = c.cache[ tbodyIndex ].normalized;
+					rowLen = cache.length;
+					for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) {
+						row = cache[ rowIndex ];
+						if ( rowFilter && !row[ c.columns ].$row.is( rowFilter ) ) {
+							continue;
+						}
+						result = true;
+						parsed = ( allColumns ) ? row.slice( 0, c.columns ) : row[ column ];
+						row = row[ c.columns ];
+						raw = ( allColumns ) ? row.raw : row.raw[ column ];
+						$cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column );
+						if ( hasCallback ) {
+							result = callback({
+								tbodyIndex : tbodyIndex,
+								rowIndex : rowIndex,
+								parsed : parsed,
+								raw : raw,
+								$row : row.$row,
+								$cell : $cell
+							});
+						}
+						if ( result !== false ) {
+							data.parsed[ data.parsed.length ] = parsed;
+							data.raw[ data.raw.length ] = raw;
+							data.$cell[ data.$cell.length ] = $cell;
+						}
+					}
+				}
+				// return everything
+				return data;
+			}
+		},
+
+		/*
+		██  ██ █████▄ █████▄ ▄████▄ ██████ ██████
+		██  ██ ██▄▄██ ██  ██ ██▄▄██   ██   ██▄▄
+		██  ██ ██▀▀▀  ██  ██ ██▀▀██   ██   ██▀▀
+		▀████▀ ██     █████▀ ██  ██   ██   ██████
+		*/
+		setHeadersCss : function( c ) {
+			var indx, column,
+				list = c.sortList,
+				len = list.length,
+				none = ts.css.sortNone + ' ' + c.cssNone,
+				css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ],
+				cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ],
+				aria = [ 'ascending', 'descending' ],
+				updateColumnSort = function($el, index) {
+					$el
+						.removeClass( none )
+						.addClass( css[ index ] )
+						.attr( 'aria-sort', aria[ index ] )
+						.find( '.' + ts.css.icon )
+						.removeClass( cssIcon[ 2 ] )
+						.addClass( cssIcon[ index ] );
+				},
+				// find the footer
+				$extras = c.$table
+					.find( 'tfoot tr' )
+					.children( 'td, th' )
+					.add( $( c.namespace + '_extra_headers' ) )
+					.removeClass( css.join( ' ' ) ),
+				// remove all header information
+				$sorted = c.$headers
+					.add( $( 'thead ' + c.namespace + '_extra_headers' ) )
+					.removeClass( css.join( ' ' ) )
+					.addClass( none )
+					.attr( 'aria-sort', 'none' )
+					.find( '.' + ts.css.icon )
+					.removeClass( cssIcon.join( ' ' ) )
+					.end();
+			// add css none to all sortable headers
+			$sorted
+				.not( '.sorter-false' )
+				.find( '.' + ts.css.icon )
+				.addClass( cssIcon[ 2 ] );
+			// add disabled css icon class
+			if ( c.cssIconDisabled ) {
+				$sorted
+					.filter( '.sorter-false' )
+					.find( '.' + ts.css.icon )
+					.addClass( c.cssIconDisabled );
+			}
+			for ( indx = 0; indx < len; indx++ ) {
+				// direction = 2 means reset!
+				if ( list[ indx ][ 1 ] !== 2 ) {
+					// multicolumn sorting updating - see #1005
+					// .not(function() {}) needs jQuery 1.4
+					// filter(function(i, el) {}) <- el is undefined in jQuery v1.2.6
+					$sorted = c.$headers.filter( function( i ) {
+						// only include headers that are in the sortList (this includes colspans)
+						var include = true,
+							$el = c.$headers.eq( i ),
+							col = parseInt( $el.attr( 'data-column' ), 10 ),
+							end = col + ts.getClosest( $el, 'th, td' )[0].colSpan;
+						for ( ; col < end; col++ ) {
+							include = include ? include || ts.isValueInArray( col, c.sortList ) > -1 : false;
+						}
+						return include;
+					});
+
+					// choose the :last in case there are nested columns
+					$sorted = $sorted
+						.not( '.sorter-false' )
+						.filter( '[data-column="' + list[ indx ][ 0 ] + '"]' + ( len === 1 ? ':last' : '' ) );
+					if ( $sorted.length ) {
+						for ( column = 0; column < $sorted.length; column++ ) {
+							if ( !$sorted[ column ].sortDisabled ) {
+								updateColumnSort( $sorted.eq( column ), list[ indx ][ 1 ] );
+							}
+						}
+					}
+					// add sorted class to footer & extra headers, if they exist
+					if ( $extras.length ) {
+						updateColumnSort( $extras.filter( '[data-column="' + list[ indx ][ 0 ] + '"]' ), list[ indx ][ 1 ] );
+					}
+				}
+			}
+			// add verbose aria labels
+			len = c.$headers.length;
+			for ( indx = 0; indx < len; indx++ ) {
+				ts.setColumnAriaLabel( c, c.$headers.eq( indx ) );
+			}
+		},
+
+		getClosest : function( $el, selector ) {
+			// jQuery v1.2.6 doesn't have closest()
+			if ( $.fn.closest ) {
+				return $el.closest( selector );
+			}
+			return $el.is( selector ) ?
+				$el :
+				$el.parents( selector ).filter( ':first' );
+		},
+
+		// nextSort (optional), lets you disable next sort text
+		setColumnAriaLabel : function( c, $header, nextSort ) {
+			if ( $header.length ) {
+				var column = parseInt( $header.attr( 'data-column' ), 10 ),
+					vars = c.sortVars[ column ],
+					tmp = $header.hasClass( ts.css.sortAsc ) ?
+						'sortAsc' :
+						$header.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone',
+					txt = $.trim( $header.text() ) + ': ' + ts.language[ tmp ];
+				if ( $header.hasClass( 'sorter-false' ) || nextSort === false ) {
+					txt += ts.language.sortDisabled;
+				} else {
+					tmp = ( vars.count + 1 ) % vars.order.length;
+					nextSort = vars.order[ tmp ];
+					// if nextSort
+					txt += ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ];
+				}
+				$header.attr( 'aria-label', txt );
+				if (vars.sortedBy) {
+					$header.attr( 'data-sortedBy', vars.sortedBy );
+				} else {
+					$header.removeAttr('data-sortedBy');
+				}
+			}
+		},
+
+		updateHeader : function( c ) {
+			var index, isDisabled, $header, col,
+				table = c.table,
+				len = c.$headers.length;
+			for ( index = 0; index < len; index++ ) {
+				$header = c.$headers.eq( index );
+				col = ts.getColumnData( table, c.headers, index, true );
+				// add 'sorter-false' class if 'parser-false' is set
+				isDisabled = ts.getData( $header, col, 'sorter' ) === 'false' || ts.getData( $header, col, 'parser' ) === 'false';
+				ts.setColumnSort( c, $header, isDisabled );
+			}
+		},
+
+		setColumnSort : function( c, $header, isDisabled ) {
+			var id = c.table.id;
+			$header[ 0 ].sortDisabled = isDisabled;
+			$header[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' )
+				.attr( 'aria-disabled', '' + isDisabled );
+			// disable tab index on disabled cells
+			if ( c.tabIndex ) {
+				if ( isDisabled ) {
+					$header.removeAttr( 'tabindex' );
+				} else {
+					$header.attr( 'tabindex', '0' );
+				}
+			}
+			// aria-controls - requires table ID
+			if ( id ) {
+				if ( isDisabled ) {
+					$header.removeAttr( 'aria-controls' );
+				} else {
+					$header.attr( 'aria-controls', id );
+				}
+			}
+		},
+
+		updateHeaderSortCount : function( c, list ) {
+			var col, dir, group, indx, primary, temp, val, order,
+				sortList = list || c.sortList,
+				len = sortList.length;
+			c.sortList = [];
+			for ( indx = 0; indx < len; indx++ ) {
+				val = sortList[ indx ];
+				// ensure all sortList values are numeric - fixes #127
+				col = parseInt( val[ 0 ], 10 );
+				// prevents error if sorton array is wrong
+				if ( col < c.columns ) {
+
+					// set order if not already defined - due to colspan header without associated header cell
+					// adding this check prevents a javascript error
+					if ( !c.sortVars[ col ].order ) {
+						if ( ts.getOrder( c.sortInitialOrder ) ) {
+							order = c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ];
+						} else {
+							order = c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ];
+						}
+						c.sortVars[ col ].order = order;
+						c.sortVars[ col ].count = 0;
+					}
+
+					order = c.sortVars[ col ].order;
+					dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ );
+					dir = dir ? dir[ 0 ] : '';
+					// 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
+					switch ( dir ) {
+						case '1' : case 'd' : // descending
+							dir = 1;
+							break;
+						case 's' : // same direction (as primary column)
+							// if primary sort is set to 's', make it ascending
+							dir = primary || 0;
+							break;
+						case 'o' :
+							temp = order[ ( primary || 0 ) % order.length ];
+							// opposite of primary column; but resets if primary resets
+							dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
+							break;
+						case 'n' :
+							dir = order[ ( ++c.sortVars[ col ].count ) % order.length ];
+							break;
+						default : // ascending
+							dir = 0;
+							break;
+					}
+					primary = indx === 0 ? dir : primary;
+					group = [ col, parseInt( dir, 10 ) || 0 ];
+					c.sortList[ c.sortList.length ] = group;
+					dir = $.inArray( group[ 1 ], order ); // fixes issue #167
+					c.sortVars[ col ].count = dir >= 0 ? dir : group[ 1 ] % order.length;
+				}
+			}
+		},
+
+		updateAll : function( c, resort, callback ) {
+			var table = c.table;
+			table.isUpdating = true;
+			ts.refreshWidgets( table, true, true );
+			ts.buildHeaders( c );
+			ts.bindEvents( table, c.$headers, true );
+			ts.bindMethods( c );
+			ts.commonUpdate( c, resort, callback );
+		},
+
+		update : function( c, resort, callback ) {
+			var table = c.table;
+			table.isUpdating = true;
+			// update sorting (if enabled/disabled)
+			ts.updateHeader( c );
+			ts.commonUpdate( c, resort, callback );
+		},
+
+		// simple header update - see #989
+		updateHeaders : function( c, callback ) {
+			c.table.isUpdating = true;
+			ts.buildHeaders( c );
+			ts.bindEvents( c.table, c.$headers, true );
+			ts.resortComplete( c, callback );
+		},
+
+		updateCell : function( c, cell, resort, callback ) {
+			// updateCell for child rows is a mess - we'll ignore them for now
+			// eventually I'll break out the "update" row cache code to make everything consistent
+			if ( $( cell ).closest( 'tr' ).hasClass( c.cssChildRow ) ) {
+				console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead');
+				return;
+			}
+			if ( ts.isEmptyObject( c.cache ) ) {
+				// empty table, do an update instead - fixes #1099
+				ts.updateHeader( c );
+				ts.commonUpdate( c, resort, callback );
+				return;
+			}
+			c.table.isUpdating = true;
+			c.$table.find( c.selectorRemove ).remove();
+			// get position from the dom
+			var tmp, indx, row, icell, cache, len,
+				$tbodies = c.$tbodies,
+				$cell = $( cell ),
+				// update cache - format: function( s, table, cell, cellIndex )
+				// no closest in jQuery v1.2.6
+				tbodyIndex = $tbodies.index( ts.getClosest( $cell, 'tbody' ) ),
+				tbcache = c.cache[ tbodyIndex ],
+				$row = ts.getClosest( $cell, 'tr' );
+			cell = $cell[ 0 ]; // in case cell is a jQuery object
+			// tbody may not exist if update is initialized while tbody is removed for processing
+			if ( $tbodies.length && tbodyIndex >= 0 ) {
+				row = $tbodies.eq( tbodyIndex ).find( 'tr' ).not( '.' + c.cssChildRow ).index( $row );
+				cache = tbcache.normalized[ row ];
+				len = $row[ 0 ].cells.length;
+				if ( len !== c.columns ) {
+					// colspan in here somewhere!
+					icell = 0;
+					tmp = false;
+					for ( indx = 0; indx < len; indx++ ) {
+						if ( !tmp && $row[ 0 ].cells[ indx ] !== cell ) {
+							icell += $row[ 0 ].cells[ indx ].colSpan;
+						} else {
+							tmp = true;
+						}
+					}
+				} else {
+					icell = $cell.index();
+				}
+				tmp = ts.getElementText( c, cell, icell ); // raw
+				cache[ c.columns ].raw[ icell ] = tmp;
+				tmp = ts.getParsedText( c, cell, icell, tmp );
+				cache[ icell ] = tmp; // parsed
+				if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) {
+					// update column max value (ignore sign)
+					tbcache.colMax[ icell ] = Math.max( Math.abs( tmp ) || 0, tbcache.colMax[ icell ] || 0 );
+				}
+				tmp = resort !== 'undefined' ? resort : c.resort;
+				if ( tmp !== false ) {
+					// widgets will be reapplied
+					ts.checkResort( c, tmp, callback );
+				} else {
+					// don't reapply widgets is resort is false, just in case it causes
+					// problems with element focus
+					ts.resortComplete( c, callback );
+				}
+			} else {
+				if ( ts.debug(c, 'core') ) {
+					console.error( 'updateCell aborted, tbody missing or not within the indicated table' );
+				}
+				c.table.isUpdating = false;
+			}
+		},
+
+		addRows : function( c, $row, resort, callback ) {
+			var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len, order,
+				cacheIndex, rowData, cells, cell, span,
+				// allow passing a row string if only one non-info tbody exists in the table
+				valid = typeof $row === 'string' && c.$tbodies.length === 1 && /<tr/.test( $row || '' ),
+				table = c.table;
+			if ( valid ) {
+				$row = $( $row );
+				c.$tbodies.append( $row );
+			} else if (
+				!$row ||
+				// row is a jQuery object?
+				!( $row instanceof $ ) ||
+				// row contained in the table?
+				( ts.getClosest( $row, 'table' )[ 0 ] !== c.table )
+			) {
+				if ( ts.debug(c, 'core') ) {
+					console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' +
+						'been added to the table, or (2) row HTML string to be added to a table with only one tbody' );
+				}
+				return false;
+			}
+			table.isUpdating = true;
+			if ( ts.isEmptyObject( c.cache ) ) {
+				// empty table, do an update instead - fixes #450
+				ts.updateHeader( c );
+				ts.commonUpdate( c, resort, callback );
+			} else {
+				rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length;
+				tbodyIndex = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) );
+				// fixes adding rows to an empty table - see issue #179
+				if ( !( c.parsers && c.parsers.length ) ) {
+					ts.setupParsers( c );
+				}
+				// add each row
+				for ( rowIndex = 0; rowIndex < rows; rowIndex++ ) {
+					cacheIndex = 0;
+					len = $row[ rowIndex ].cells.length;
+					order = c.cache[ tbodyIndex ].normalized.length;
+					cells = [];
+					rowData = {
+						child : [],
+						raw : [],
+						$row : $row.eq( rowIndex ),
+						order : order
+					};
+					// add each cell
+					for ( cellIndex = 0; cellIndex < len; cellIndex++ ) {
+						cell = $row[ rowIndex ].cells[ cellIndex ];
+						txt = ts.getElementText( c, cell, cacheIndex );
+						rowData.raw[ cacheIndex ] = txt;
+						val = ts.getParsedText( c, cell, cacheIndex, txt );
+						cells[ cacheIndex ] = val;
+						if ( ( c.parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) {
+							// update column max value (ignore sign)
+							c.cache[ tbodyIndex ].colMax[ cacheIndex ] =
+								Math.max( Math.abs( val ) || 0, c.cache[ tbodyIndex ].colMax[ cacheIndex ] || 0 );
+						}
+						span = cell.colSpan - 1;
+						if ( span > 0 ) {
+							cacheIndex += span;
+						}
+						cacheIndex++;
+					}
+					// add the row data to the end
+					cells[ c.columns ] = rowData;
+					// update cache
+					c.cache[ tbodyIndex ].normalized[ order ] = cells;
+				}
+				// resort using current settings
+				ts.checkResort( c, resort, callback );
+			}
+		},
+
+		updateCache : function( c, callback, $tbodies ) {
+			// rebuild parsers
+			if ( !( c.parsers && c.parsers.length ) ) {
+				ts.setupParsers( c, $tbodies );
+			}
+			// rebuild the cache map
+			ts.buildCache( c, callback, $tbodies );
+		},
+
+		// init flag (true) used by pager plugin to prevent widget application
+		// renamed from appendToTable
+		appendCache : function( c, init ) {
+			var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime,
+				table = c.table,
+				$tbodies = c.$tbodies,
+				rows = [],
+				cache = c.cache;
+			// empty table - fixes #206/#346
+			if ( ts.isEmptyObject( cache ) ) {
+				// run pager appender in case the table was just emptied
+				return c.appender ? c.appender( table, rows ) :
+					table.isUpdating ? c.$table.triggerHandler( 'updateComplete', table ) : ''; // Fixes #532
+			}
+			if ( ts.debug(c, 'core') ) {
+				appendTime = new Date();
+			}
+			for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
+				$tbody = $tbodies.eq( tbodyIndex );
+				if ( $tbody.length ) {
+					// detach tbody for manipulation
+					$curTbody = ts.processTbody( table, $tbody, true );
+					parsed = cache[ tbodyIndex ].normalized;
+					totalRows = parsed.length;
+					for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) {
+						rows[rows.length] = parsed[ rowIndex ][ c.columns ].$row;
+						// removeRows used by the pager plugin; don't render if using ajax - fixes #411
+						if ( !c.appender || ( c.pager && !c.pager.removeRows && !c.pager.ajax ) ) {
+							$curTbody.append( parsed[ rowIndex ][ c.columns ].$row );
+						}
+					}
+					// restore tbody
+					ts.processTbody( table, $curTbody, false );
+				}
+			}
+			if ( c.appender ) {
+				c.appender( table, rows );
+			}
+			if ( ts.debug(c, 'core') ) {
+				console.log( 'Rebuilt table' + ts.benchmark( appendTime ) );
+			}
+			// apply table widgets; but not before ajax completes
+			if ( !init && !c.appender ) {
+				ts.applyWidget( table );
+			}
+			if ( table.isUpdating ) {
+				c.$table.triggerHandler( 'updateComplete', table );
+			}
+		},
+
+		commonUpdate : function( c, resort, callback ) {
+			// remove rows/elements before update
+			c.$table.find( c.selectorRemove ).remove();
+			// rebuild parsers
+			ts.setupParsers( c );
+			// rebuild the cache map
+			ts.buildCache( c );
+			ts.checkResort( c, resort, callback );
+		},
+
+		/*
+		▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄
+		▀█▄    ██  ██ ██▄▄██   ██   ██ ██  ██ ██ ▄▄▄
+		   ▀█▄ ██  ██ ██▀██    ██   ██ ██  ██ ██ ▀██
+		█████▀ ▀████▀ ██  ██   ██   ██ ██  ██ ▀████▀
+		*/
+		initSort : function( c, cell, event ) {
+			if ( c.table.isUpdating ) {
+				// let any updates complete before initializing a sort
+				return setTimeout( function() {
+					ts.initSort( c, cell, event );
+				}, 50 );
+			}
+
+			var arry, indx, headerIndx, dir, temp, tmp, $header,
+				notMultiSort = !event[ c.sortMultiSortKey ],
+				table = c.table,
+				len = c.$headers.length,
+				th = ts.getClosest( $( cell ), 'th, td' ),
+				col = parseInt( th.attr( 'data-column' ), 10 ),
+				sortedBy = event.type === 'mouseup' ? 'user' : event.type,
+				order = c.sortVars[ col ].order;
+			th = th[0];
+			// Only call sortStart if sorting is enabled
+			c.$table.triggerHandler( 'sortStart', table );
+			// get current column sort order
+			tmp = ( c.sortVars[ col ].count + 1 ) % order.length;
+			c.sortVars[ col ].count = event[ c.sortResetKey ] ? 2 : tmp;
+			// reset all sorts on non-current column - issue #30
+			if ( c.sortRestart ) {
+				for ( headerIndx = 0; headerIndx < len; headerIndx++ ) {
+					$header = c.$headers.eq( headerIndx );
+					tmp = parseInt( $header.attr( 'data-column' ), 10 );
+					// only reset counts on columns that weren't just clicked on and if not included in a multisort
+					if ( col !== tmp && ( notMultiSort || $header.hasClass( ts.css.sortNone ) ) ) {
+						c.sortVars[ tmp ].count = -1;
+					}
+				}
+			}
+			// user only wants to sort on one column
+			if ( notMultiSort ) {
+				$.each( c.sortVars, function( i ) {
+					c.sortVars[ i ].sortedBy = '';
+				});
+				// flush the sort list
+				c.sortList = [];
+				c.last.sortList = [];
+				if ( c.sortForce !== null ) {
+					arry = c.sortForce;
+					for ( indx = 0; indx < arry.length; indx++ ) {
+						if ( arry[ indx ][ 0 ] !== col ) {
+							c.sortList[ c.sortList.length ] = arry[ indx ];
+							c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortForce';
+						}
+					}
+				}
+				// add column to sort list
+				dir = order[ c.sortVars[ col ].count ];
+				if ( dir < 2 ) {
+					c.sortList[ c.sortList.length ] = [ col, dir ];
+					c.sortVars[ col ].sortedBy = sortedBy;
+					// add other columns if header spans across multiple
+					if ( th.colSpan > 1 ) {
+						for ( indx = 1; indx < th.colSpan; indx++ ) {
+							c.sortList[ c.sortList.length ] = [ col + indx, dir ];
+							// update count on columns in colSpan
+							c.sortVars[ col + indx ].count = $.inArray( dir, order );
+							c.sortVars[ col + indx ].sortedBy = sortedBy;
+						}
+					}
+				}
+				// multi column sorting
+			} else {
+				// get rid of the sortAppend before adding more - fixes issue #115 & #523
+				c.sortList = $.extend( [], c.last.sortList );
+
+				// the user has clicked on an already sorted column
+				if ( ts.isValueInArray( col, c.sortList ) >= 0 ) {
+					// reverse the sorting direction
+					c.sortVars[ col ].sortedBy = sortedBy;
+					for ( indx = 0; indx < c.sortList.length; indx++ ) {
+						tmp = c.sortList[ indx ];
+						if ( tmp[ 0 ] === col ) {
+							// order.count seems to be incorrect when compared to cell.count
+							tmp[ 1 ] = order[ c.sortVars[ col ].count ];
+							if ( tmp[1] === 2 ) {
+								c.sortList.splice( indx, 1 );
+								c.sortVars[ col ].count = -1;
+							}
+						}
+					}
+				} else {
+					// add column to sort list array
+					dir = order[ c.sortVars[ col ].count ];
+					c.sortVars[ col ].sortedBy = sortedBy;
+					if ( dir < 2 ) {
+						c.sortList[ c.sortList.length ] = [ col, dir ];
+						// add other columns if header spans across multiple
+						if ( th.colSpan > 1 ) {
+							for ( indx = 1; indx < th.colSpan; indx++ ) {
+								c.sortList[ c.sortList.length ] = [ col + indx, dir ];
+								// update count on columns in colSpan
+								c.sortVars[ col + indx ].count = $.inArray( dir, order );
+								c.sortVars[ col + indx ].sortedBy = sortedBy;
+							}
+						}
+					}
+				}
+			}
+			// save sort before applying sortAppend
+			c.last.sortList = $.extend( [], c.sortList );
+			if ( c.sortList.length && c.sortAppend ) {
+				arry = $.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ];
+				if ( !ts.isEmptyObject( arry ) ) {
+					for ( indx = 0; indx < arry.length; indx++ ) {
+						if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) {
+							dir = arry[ indx ][ 1 ];
+							temp = ( '' + dir ).match( /^(a|d|s|o|n)/ );
+							if ( temp ) {
+								tmp = c.sortList[ 0 ][ 1 ];
+								switch ( temp[ 0 ] ) {
+									case 'd' :
+										dir = 1;
+										break;
+									case 's' :
+										dir = tmp;
+										break;
+									case 'o' :
+										dir = tmp === 0 ? 1 : 0;
+										break;
+									case 'n' :
+										dir = ( tmp + 1 ) % order.length;
+										break;
+									default:
+										dir = 0;
+										break;
+								}
+							}
+							c.sortList[ c.sortList.length ] = [ arry[ indx ][ 0 ], dir ];
+							c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortAppend';
+						}
+					}
+				}
+			}
+			// sortBegin event triggered immediately before the sort
+			c.$table.triggerHandler( 'sortBegin', table );
+			// setTimeout needed so the processing icon shows up
+			setTimeout( function() {
+				// set css for headers
+				ts.setHeadersCss( c );
+				ts.multisort( c );
+				ts.appendCache( c );
+				c.$table.triggerHandler( 'sortBeforeEnd', table );
+				c.$table.triggerHandler( 'sortEnd', table );
+			}, 1 );
+		},
+
+		// sort multiple columns
+		multisort : function( c ) { /*jshint loopfunc:true */
+			var tbodyIndex, sortTime, colMax, rows, tmp,
+				table = c.table,
+				sorter = [],
+				dir = 0,
+				textSorter = c.textSorter || '',
+				sortList = c.sortList,
+				sortLen = sortList.length,
+				len = c.$tbodies.length;
+			if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) {
+				// empty table - fixes #206/#346
+				return;
+			}
+			if ( ts.debug(c, 'core') ) { sortTime = new Date(); }
+			// cache textSorter to optimize speed
+			if ( typeof textSorter === 'object' ) {
+				colMax = c.columns;
+				while ( colMax-- ) {
+					tmp = ts.getColumnData( table, textSorter, colMax );
+					if ( typeof tmp === 'function' ) {
+						sorter[ colMax ] = tmp;
+					}
+				}
+			}
+			for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) {
+				colMax = c.cache[ tbodyIndex ].colMax;
+				rows = c.cache[ tbodyIndex ].normalized;
+
+				rows.sort( function( a, b ) {
+					var sortIndex, num, col, order, sort, x, y;
+					// rows is undefined here in IE, so don't use it!
+					for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) {
+						col = sortList[ sortIndex ][ 0 ];
+						order = sortList[ sortIndex ][ 1 ];
+						// sort direction, true = asc, false = desc
+						dir = order === 0;
+
+						if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) {
+							return a[ c.columns ].order - b[ c.columns ].order;
+						}
+
+						// fallback to natural sort since it is more robust
+						num = /n/i.test( ts.getSortType( c.parsers, col ) );
+						if ( num && c.strings[ col ] ) {
+							// sort strings in numerical columns
+							if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) {
+								num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 );
+							} else {
+								num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0;
+							}
+							// fall back to built-in numeric sort
+							// var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table );
+							sort = c.numberSorter ? c.numberSorter( a[ col ], b[ col ], dir, colMax[ col ], table ) :
+								ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], num, colMax[ col ], col, c );
+						} else {
+							// set a & b depending on sort direction
+							x = dir ? a : b;
+							y = dir ? b : a;
+							// text sort function
+							if ( typeof textSorter === 'function' ) {
+								// custom OVERALL text sorter
+								sort = textSorter( x[ col ], y[ col ], dir, col, table );
+							} else if ( typeof sorter[ col ] === 'function' ) {
+								// custom text sorter for a SPECIFIC COLUMN
+								sort = sorter[ col ]( x[ col ], y[ col ], dir, col, table );
+							} else {
+								// fall back to natural sort
+								sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ] || '', b[ col ] || '', col, c );
+							}
+						}
+						if ( sort ) { return sort; }
+					}
+					return a[ c.columns ].order - b[ c.columns ].order;
+				});
+			}
+			if ( ts.debug(c, 'core') ) {
+				console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) );
+			}
+		},
+
+		resortComplete : function( c, callback ) {
+			if ( c.table.isUpdating ) {
+				c.$table.triggerHandler( 'updateComplete', c.table );
+			}
+			if ( $.isFunction( callback ) ) {
+				callback( c.table );
+			}
+		},
+
+		checkResort : function( c, resort, callback ) {
+			var sortList = $.isArray( resort ) ? resort : c.sortList,
+				// if no resort parameter is passed, fallback to config.resort (true by default)
+				resrt = typeof resort === 'undefined' ? c.resort : resort;
+			// don't try to resort if the table is still processing
+			// this will catch spamming of the updateCell method
+			if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) {
+				if ( sortList.length ) {
+					ts.sortOn( c, sortList, function() {
+						ts.resortComplete( c, callback );
+					}, true );
+				} else {
+					ts.sortReset( c, function() {
+						ts.resortComplete( c, callback );
+						ts.applyWidget( c.table, false );
+					} );
+				}
+			} else {
+				ts.resortComplete( c, callback );
+				ts.applyWidget( c.table, false );
+			}
+		},
+
+		sortOn : function( c, list, callback, init ) {
+			var indx,
+				table = c.table;
+			c.$table.triggerHandler( 'sortStart', table );
+			for (indx = 0; indx < c.columns; indx++) {
+				c.sortVars[ indx ].sortedBy = ts.isValueInArray( indx, list ) > -1 ? 'sorton' : '';
+			}
+			// update header count index
+			ts.updateHeaderSortCount( c, list );
+			// set css for headers
+			ts.setHeadersCss( c );
+			// fixes #346
+			if ( c.delayInit && ts.isEmptyObject( c.cache ) ) {
+				ts.buildCache( c );
+			}
+			c.$table.triggerHandler( 'sortBegin', table );
+			// sort the table and append it to the dom
+			ts.multisort( c );
+			ts.appendCache( c, init );
+			c.$table.triggerHandler( 'sortBeforeEnd', table );
+			c.$table.triggerHandler( 'sortEnd', table );
+			ts.applyWidget( table );
+			if ( $.isFunction( callback ) ) {
+				callback( table );
+			}
+		},
+
+		sortReset : function( c, callback ) {
+			c.sortList = [];
+			var indx;
+			for (indx = 0; indx < c.columns; indx++) {
+				c.sortVars[ indx ].count = -1;
+				c.sortVars[ indx ].sortedBy = '';
+			}
+			ts.setHeadersCss( c );
+			ts.multisort( c );
+			ts.appendCache( c );
+			if ( $.isFunction( callback ) ) {
+				callback( c.table );
+			}
+		},
+
+		getSortType : function( parsers, column ) {
+			return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : '';
+		},
+
+		getOrder : function( val ) {
+			// look for 'd' in 'desc' order; return true
+			return ( /^d/i.test( val ) || val === 1 );
+		},
+
+		// Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
+		sortNatural : function( a, b ) {
+			if ( a === b ) { return 0; }
+			a = ( a || '' ).toString();
+			b = ( b || '' ).toString();
+			var aNum, bNum, aFloat, bFloat, indx, max,
+				regex = ts.regex;
+			// first try and sort Hex codes
+			if ( regex.hex.test( b ) ) {
+				aNum = parseInt( a.match( regex.hex ), 16 );
+				bNum = parseInt( b.match( regex.hex ), 16 );
+				if ( aNum < bNum ) { return -1; }
+				if ( aNum > bNum ) { return 1; }
+			}
+			// chunk/tokenize
+			aNum = a.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' );
+			bNum = b.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' );
+			max = Math.max( aNum.length, bNum.length );
+			// natural sorting through split numeric strings and default strings
+			for ( indx = 0; indx < max; indx++ ) {
+				// find floats not starting with '0', string or 0 if not defined
+				aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0;
+				bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0;
+				// handle numeric vs string comparison - number < string - (Kyle Adams)
+				if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; }
+				// rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
+				if ( typeof aFloat !== typeof bFloat ) {
+					aFloat += '';
+					bFloat += '';
+				}
+				if ( aFloat < bFloat ) { return -1; }
+				if ( aFloat > bFloat ) { return 1; }
+			}
+			return 0;
+		},
+
+		sortNaturalAsc : function( a, b, col, c ) {
+			if ( a === b ) { return 0; }
+			var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
+			if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; }
+			if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; }
+			return ts.sortNatural( a, b );
+		},
+
+		sortNaturalDesc : function( a, b, col, c ) {
+			if ( a === b ) { return 0; }
+			var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
+			if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; }
+			if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; }
+			return ts.sortNatural( b, a );
+		},
+
+		// basic alphabetical sort
+		sortText : function( a, b ) {
+			return a > b ? 1 : ( a < b ? -1 : 0 );
+		},
+
+		// return text string value by adding up ascii value
+		// so the text is somewhat sorted when using a digital sort
+		// this is NOT an alphanumeric sort
+		getTextValue : function( val, num, max ) {
+			if ( max ) {
+				// make sure the text value is greater than the max numerical value (max)
+				var indx,
+					len = val ? val.length : 0,
+					n = max + num;
+				for ( indx = 0; indx < len; indx++ ) {
+					n += val.charCodeAt( indx );
+				}
+				return num * n;
+			}
+			return 0;
+		},
+
+		sortNumericAsc : function( a, b, num, max, col, c ) {
+			if ( a === b ) { return 0; }
+			var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
+			if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; }
+			if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; }
+			if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); }
+			if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); }
+			return a - b;
+		},
+
+		sortNumericDesc : function( a, b, num, max, col, c ) {
+			if ( a === b ) { return 0; }
+			var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ];
+			if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; }
+			if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; }
+			if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); }
+			if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); }
+			return b - a;
+		},
+
+		sortNumeric : function( a, b ) {
+			return a - b;
+		},
+
+		/*
+		██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████
+		██ ██ ██ ██ ██  ██ ██ ▄▄▄ ██▄▄     ██   ▀█▄
+		██ ██ ██ ██ ██  ██ ██ ▀██ ██▀▀     ██      ▀█▄
+		███████▀ ██ █████▀ ▀████▀ ██████   ██   █████▀
+		*/
+		addWidget : function( widget ) {
+			if ( widget.id && !ts.isEmptyObject( ts.getWidgetById( widget.id ) ) ) {
+				console.warn( '"' + widget.id + '" widget was loaded more than once!' );
+			}
+			ts.widgets[ ts.widgets.length ] = widget;
+		},
+
+		hasWidget : function( $table, name ) {
+			$table = $( $table );
+			return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false;
+		},
+
+		getWidgetById : function( name ) {
+			var indx, widget,
+				len = ts.widgets.length;
+			for ( indx = 0; indx < len; indx++ ) {
+				widget = ts.widgets[ indx ];
+				if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) {
+					return widget;
+				}
+			}
+		},
+
+		applyWidgetOptions : function( table ) {
+			var indx, widget, wo,
+				c = table.config,
+				len = c.widgets.length;
+			if ( len ) {
+				for ( indx = 0; indx < len; indx++ ) {
+					widget = ts.getWidgetById( c.widgets[ indx ] );
+					if ( widget && widget.options ) {
+						wo = $.extend( true, {}, widget.options );
+						c.widgetOptions = $.extend( true, wo, c.widgetOptions );
+						// add widgetOptions to defaults for option validator
+						$.extend( true, ts.defaults.widgetOptions, widget.options );
+					}
+				}
+			}
+		},
+
+		addWidgetFromClass : function( table ) {
+			var len, indx,
+				c = table.config,
+				// look for widgets to apply from table class
+				// don't match from 'ui-widget-content'; use \S instead of \w to include widgets
+				// with dashes in the name, e.g. "widget-test-2" extracts out "test-2"
+				regex = '^' + c.widgetClass.replace( ts.regex.templateName, '(\\S+)+' ) + '$',
+				widgetClass = new RegExp( regex, 'g' ),
+				// split up table class (widget id's can include dashes) - stop using match
+				// otherwise only one widget gets extracted, see #1109
+				widgets = ( table.className || '' ).split( ts.regex.spaces );
+			if ( widgets.length ) {
+				len = widgets.length;
+				for ( indx = 0; indx < len; indx++ ) {
+					if ( widgets[ indx ].match( widgetClass ) ) {
+						c.widgets[ c.widgets.length ] = widgets[ indx ].replace( widgetClass, '$1' );
+					}
+				}
+			}
+		},
+
+		applyWidgetId : function( table, id, init ) {
+			table = $(table)[0];
+			var applied, time, name,
+				c = table.config,
+				wo = c.widgetOptions,
+				debug = ts.debug(c, 'core'),
+				widget = ts.getWidgetById( id );
+			if ( widget ) {
+				name = widget.id;
+				applied = false;
+				// add widget name to option list so it gets reapplied after sorting, filtering, etc
+				if ( $.inArray( name, c.widgets ) < 0 ) {
+					c.widgets[ c.widgets.length ] = name;
+				}
+				if ( debug ) { time = new Date(); }
+
+				if ( init || !( c.widgetInit[ name ] ) ) {
+					// set init flag first to prevent calling init more than once (e.g. pager)
+					c.widgetInit[ name ] = true;
+					if ( table.hasInitialized ) {
+						// don't reapply widget options on tablesorter init
+						ts.applyWidgetOptions( table );
+					}
+					if ( typeof widget.init === 'function' ) {
+						applied = true;
+						if ( debug ) {
+							console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' );
+						}
+						widget.init( table, widget, c, wo );
+					}
+				}
+				if ( !init && typeof widget.format === 'function' ) {
+					applied = true;
+					if ( debug ) {
+						console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' );
+					}
+					widget.format( table, c, wo, false );
+				}
+				if ( debug ) {
+					if ( applied ) {
+						console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) );
+						if ( console.groupEnd ) { console.groupEnd(); }
+					}
+				}
+			}
+		},
+
+		applyWidget : function( table, init, callback ) {
+			table = $( table )[ 0 ]; // in case this is called externally
+			var indx, len, names, widget, time,
+				c = table.config,
+				debug = ts.debug(c, 'core'),
+				widgets = [];
+			// prevent numerous consecutive widget applications
+			if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) {
+				return;
+			}
+			if ( debug ) { time = new Date(); }
+			ts.addWidgetFromClass( table );
+			// prevent "tablesorter-ready" from firing multiple times in a row
+			clearTimeout( c.timerReady );
+			if ( c.widgets.length ) {
+				table.isApplyingWidgets = true;
+				// ensure unique widget ids
+				c.widgets = $.grep( c.widgets, function( val, index ) {
+					return $.inArray( val, c.widgets ) === index;
+				});
+				names = c.widgets || [];
+				len = names.length;
+				// build widget array & add priority as needed
+				for ( indx = 0; indx < len; indx++ ) {
+					widget = ts.getWidgetById( names[ indx ] );
+					if ( widget && widget.id ) {
+						// set priority to 10 if not defined
+						if ( !widget.priority ) { widget.priority = 10; }
+						widgets[ indx ] = widget;
+					} else if ( debug ) {
+						console.warn( '"' + names[ indx ] + '" was enabled, but the widget code has not been loaded!' );
+					}
+				}
+				// sort widgets by priority
+				widgets.sort( function( a, b ) {
+					return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
+				});
+				// add/update selected widgets
+				len = widgets.length;
+				if ( debug ) {
+					console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' );
+				}
+				for ( indx = 0; indx < len; indx++ ) {
+					widget = widgets[ indx ];
+					if ( widget && widget.id ) {
+						ts.applyWidgetId( table, widget.id, init );
+					}
+				}
+				if ( debug && console.groupEnd ) { console.groupEnd(); }
+			}
+			c.timerReady = setTimeout( function() {
+				table.isApplyingWidgets = false;
+				$.data( table, 'lastWidgetApplication', new Date() );
+				c.$table.triggerHandler( 'tablesorter-ready' );
+				// callback executed on init only
+				if ( !init && typeof callback === 'function' ) {
+					callback( table );
+				}
+				if ( debug ) {
+					widget = c.widgets.length;
+					console.log( 'Completed ' +
+						( init === true ? 'initializing ' : 'applying ' ) + widget +
+						' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) );
+				}
+			}, 10 );
+		},
+
+		removeWidget : function( table, name, refreshing ) {
+			table = $( table )[ 0 ];
+			var index, widget, indx, len,
+				c = table.config;
+			// if name === true, add all widgets from $.tablesorter.widgets
+			if ( name === true ) {
+				name = [];
+				len = ts.widgets.length;
+				for ( indx = 0; indx < len; indx++ ) {
+					widget = ts.widgets[ indx ];
+					if ( widget && widget.id ) {
+						name[ name.length ] = widget.id;
+					}
+				}
+			} else {
+				// name can be either an array of widgets names,
+				// or a space/comma separated list of widget names
+				name = ( $.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ );
+			}
+			len = name.length;
+			for ( index = 0; index < len; index++ ) {
+				widget = ts.getWidgetById( name[ index ] );
+				indx = $.inArray( name[ index ], c.widgets );
+				// don't remove the widget from config.widget if refreshing
+				if ( indx >= 0 && refreshing !== true ) {
+					c.widgets.splice( indx, 1 );
+				}
+				if ( widget && widget.remove ) {
+					if ( ts.debug(c, 'core') ) {
+						console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' );
+					}
+					widget.remove( table, c, c.widgetOptions, refreshing );
+					c.widgetInit[ name[ index ] ] = false;
+				}
+			}
+			c.$table.triggerHandler( 'widgetRemoveEnd', table );
+		},
+
+		refreshWidgets : function( table, doAll, dontapply ) {
+			table = $( table )[ 0 ]; // see issue #243
+			var indx, widget,
+				c = table.config,
+				curWidgets = c.widgets,
+				widgets = ts.widgets,
+				len = widgets.length,
+				list = [],
+				callback = function( table ) {
+					$( table ).triggerHandler( 'refreshComplete' );
+				};
+			// remove widgets not defined in config.widgets, unless doAll is true
+			for ( indx = 0; indx < len; indx++ ) {
+				widget = widgets[ indx ];
+				if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) {
+					list[ list.length ] = widget.id;
+				}
+			}
+			ts.removeWidget( table, list.join( ',' ), true );
+			if ( dontapply !== true ) {
+				// call widget init if
+				ts.applyWidget( table, doAll || false, callback );
+				if ( doAll ) {
+					// apply widget format
+					ts.applyWidget( table, false, callback );
+				}
+			} else {
+				callback( table );
+			}
+		},
+
+		/*
+		██  ██ ██████ ██ ██     ██ ██████ ██ ██████ ▄█████
+		██  ██   ██   ██ ██     ██   ██   ██ ██▄▄   ▀█▄
+		██  ██   ██   ██ ██     ██   ██   ██ ██▀▀      ▀█▄
+		▀████▀   ██   ██ ██████ ██   ██   ██ ██████ █████▀
+		*/
+		benchmark : function( diff ) {
+			return ( ' (' + ( new Date().getTime() - diff.getTime() ) + ' ms)' );
+		},
+		// deprecated ts.log
+		log : function() {
+			console.log( arguments );
+		},
+		debug : function(c, name) {
+			return c && (
+				c.debug === true ||
+				typeof c.debug === 'string' && c.debug.indexOf(name) > -1
+			);
+		},
+
+		// $.isEmptyObject from jQuery v1.4
+		isEmptyObject : function( obj ) {
+			/*jshint forin: false */
+			for ( var name in obj ) {
+				return false;
+			}
+			return true;
+		},
+
+		isValueInArray : function( column, arry ) {
+			var indx,
+				len = arry && arry.length || 0;
+			for ( indx = 0; indx < len; indx++ ) {
+				if ( arry[ indx ][ 0 ] === column ) {
+					return indx;
+				}
+			}
+			return -1;
+		},
+
+		formatFloat : function( str, table ) {
+			if ( typeof str !== 'string' || str === '' ) { return str; }
+			// allow using formatFloat without a table; defaults to US number format
+			var num,
+				usFormat = table && table.config ? table.config.usNumberFormat !== false :
+					typeof table !== 'undefined' ? table : true;
+			if ( usFormat ) {
+				// US Format - 1,234,567.89 -> 1234567.89
+				str = str.replace( ts.regex.comma, '' );
+			} else {
+				// German Format = 1.234.567,89 -> 1234567.89
+				// French Format = 1 234 567,89 -> 1234567.89
+				str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' );
+			}
+			if ( ts.regex.digitNegativeTest.test( str ) ) {
+				// make (#) into a negative number -> (10) = -10
+				str = str.replace( ts.regex.digitNegativeReplace, '-$1' );
+			}
+			num = parseFloat( str );
+			// return the text instead of zero
+			return isNaN( num ) ? $.trim( str ) : num;
+		},
+
+		isDigit : function( str ) {
+			// replace all unwanted chars and match
+			return isNaN( str ) ?
+				ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) :
+				str !== '';
+		},
+
+		// computeTableHeaderCellIndexes from:
+		// http://www.javascripttoolbox.com/lib/table/examples.php
+		// http://www.javascripttoolbox.com/temp/table_cellindex.html
+		computeColumnIndex : function( $rows, c ) {
+			var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol,
+				// total columns has been calculated, use it to set the matrixrow
+				columns = c && c.columns || 0,
+				matrix = [],
+				matrixrow = new Array( columns );
+			for ( i = 0; i < $rows.length; i++ ) {
+				cells = $rows[ i ].cells;
+				for ( j = 0; j < cells.length; j++ ) {
+					cell = cells[ j ];
+					rowIndex = i;
+					rowSpan = cell.rowSpan || 1;
+					colSpan = cell.colSpan || 1;
+					if ( typeof matrix[ rowIndex ] === 'undefined' ) {
+						matrix[ rowIndex ] = [];
+					}
+					// Find first available column in the first row
+					for ( k = 0; k < matrix[ rowIndex ].length + 1; k++ ) {
+						if ( typeof matrix[ rowIndex ][ k ] === 'undefined' ) {
+							firstAvailCol = k;
+							break;
+						}
+					}
+					// jscs:disable disallowEmptyBlocks
+					if ( columns && cell.cellIndex === firstAvailCol ) {
+						// don't to anything
+					} else if ( cell.setAttribute ) {
+						// jscs:enable disallowEmptyBlocks
+						// add data-column (setAttribute = IE8+)
+						cell.setAttribute( 'data-column', firstAvailCol );
+					} else {
+						// remove once we drop support for IE7 - 1/12/2016
+						$( cell ).attr( 'data-column', firstAvailCol );
+					}
+					for ( k = rowIndex; k < rowIndex + rowSpan; k++ ) {
+						if ( typeof matrix[ k ] === 'undefined' ) {
+							matrix[ k ] = [];
+						}
+						matrixrow = matrix[ k ];
+						for ( l = firstAvailCol; l < firstAvailCol + colSpan; l++ ) {
+							matrixrow[ l ] = 'x';
+						}
+					}
+				}
+			}
+			ts.checkColumnCount($rows, matrix, matrixrow.length);
+			return matrixrow.length;
+		},
+
+		checkColumnCount : function($rows, matrix, columns) {
+			// this DOES NOT report any tbody column issues, except for the math and
+			// and column selector widgets
+			var i, len,
+				valid = true,
+				cells = [];
+			for ( i = 0; i < matrix.length; i++ ) {
+				// some matrix entries are undefined when testing the footer because
+				// it is using the rowIndex property
+				if ( matrix[i] ) {
+					len = matrix[i].length;
+					if ( matrix[i].length !== columns ) {
+						valid = false;
+						break;
+					}
+				}
+			}
+			if ( !valid ) {
+				$rows.each( function( indx, el ) {
+					var cell = el.parentElement.nodeName;
+					if ( cells.indexOf( cell ) < 0 ) {
+						cells.push( cell );
+					}
+				});
+				console.error(
+					'Invalid or incorrect number of columns in the ' +
+					cells.join( ' or ' ) + '; expected ' + columns +
+					', but found ' + len + ' columns'
+				);
+			}
+		},
+
+		// automatically add a colgroup with col elements set to a percentage width
+		fixColumnWidth : function( table ) {
+			table = $( table )[ 0 ];
+			var overallWidth, percent, $tbodies, len, index,
+				c = table.config,
+				$colgroup = c.$table.children( 'colgroup' );
+			// remove plugin-added colgroup, in case we need to refresh the widths
+			if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) {
+				$colgroup.remove();
+			}
+			if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) {
+				$colgroup = $( '<colgroup class="' + ts.css.colgroup + '">' );
+				overallWidth = c.$table.width();
+				// only add col for visible columns - fixes #371
+				$tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' );
+				len = $tbodies.length;
+				for ( index = 0; index < len; index++ ) {
+					percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%';
+					$colgroup.append( $( '<col>' ).css( 'width', percent ) );
+				}
+				c.$table.prepend( $colgroup );
+			}
+		},
+
+		// get sorter, string, empty, etc options for each column from
+		// jQuery data, metadata, header option or header class name ('sorter-false')
+		// priority = jQuery data > meta > headers option > header class name
+		getData : function( header, configHeader, key ) {
+			var meta, cl4ss,
+				val = '',
+				$header = $( header );
+			if ( !$header.length ) { return ''; }
+			meta = $.metadata ? $header.metadata() : false;
+			cl4ss = ' ' + ( $header.attr( 'class' ) || '' );
+			if ( typeof $header.data( key ) !== 'undefined' ||
+				typeof $header.data( key.toLowerCase() ) !== 'undefined' ) {
+				// 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
+				// 'data-sort-initial-order' is assigned to 'sortInitialOrder'
+				val += $header.data( key ) || $header.data( key.toLowerCase() );
+			} else if ( meta && typeof meta[ key ] !== 'undefined' ) {
+				val += meta[ key ];
+			} else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) {
+				val += configHeader[ key ];
+			} else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) {
+				// include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
+				val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || '';
+			}
+			return $.trim( val );
+		},
+
+		getColumnData : function( table, obj, indx, getCell, $headers ) {
+			if ( typeof obj !== 'object' || obj === null ) {
+				return obj;
+			}
+			table = $( table )[ 0 ];
+			var $header, key,
+				c = table.config,
+				$cells = ( $headers || c.$headers ),
+				// c.$headerIndexed is not defined initially
+				$cell = c.$headerIndexed && c.$headerIndexed[ indx ] ||
+					$cells.find( '[data-column="' + indx + '"]:last' );
+			if ( typeof obj[ indx ] !== 'undefined' ) {
+				return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ];
+			}
+			for ( key in obj ) {
+				if ( typeof key === 'string' ) {
+					$header = $cell
+						// header cell with class/id
+						.filter( key )
+						// find elements within the header cell with cell/id
+						.add( $cell.find( key ) );
+					if ( $header.length ) {
+						return obj[ key ];
+					}
+				}
+			}
+			return;
+		},
+
+		// *** Process table ***
+		// add processing indicator
+		isProcessing : function( $table, toggle, $headers ) {
+			$table = $( $table );
+			var c = $table[ 0 ].config,
+				// default to all headers
+				$header = $headers || $table.find( '.' + ts.css.header );
+			if ( toggle ) {
+				// don't use sortList if custom $headers used
+				if ( typeof $headers !== 'undefined' && c.sortList.length > 0 ) {
+					// get headers from the sortList
+					$header = $header.filter( function() {
+						// get data-column from attr to keep compatibility with jQuery 1.2.6
+						return this.sortDisabled ?
+							false :
+							ts.isValueInArray( parseFloat( $( this ).attr( 'data-column' ) ), c.sortList ) >= 0;
+					});
+				}
+				$table.add( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing );
+			} else {
+				$table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing );
+			}
+		},
+
+		// detach tbody but save the position
+		// don't use tbody because there are portions that look for a tbody index (updateCell)
+		processTbody : function( table, $tb, getIt ) {
+			table = $( table )[ 0 ];
+			if ( getIt ) {
+				table.isProcessing = true;
+				$tb.before( '<colgroup class="tablesorter-savemyplace"/>' );
+				return $.fn.detach ? $tb.detach() : $tb.remove();
+			}
+			var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' );
+			$tb.insertAfter( holdr );
+			holdr.remove();
+			table.isProcessing = false;
+		},
+
+		clearTableBody : function( table ) {
+			$( table )[ 0 ].config.$tbodies.children().detach();
+		},
+
+		// used when replacing accented characters during sorting
+		characterEquivalents : {
+			'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
+			'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
+			'c' : '\u00e7\u0107\u010d', // çćč
+			'C' : '\u00c7\u0106\u010c', // ÇĆČ
+			'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
+			'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
+			'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
+			'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
+			'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō
+			'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ
+			'ss': '\u00df', // ß (s sharp)
+			'SS': '\u1e9e', // ẞ (Capital sharp s)
+			'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
+			'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
+		},
+
+		replaceAccents : function( str ) {
+			var chr,
+				acc = '[',
+				eq = ts.characterEquivalents;
+			if ( !ts.characterRegex ) {
+				ts.characterRegexArray = {};
+				for ( chr in eq ) {
+					if ( typeof chr === 'string' ) {
+						acc += eq[ chr ];
+						ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' );
+					}
+				}
+				ts.characterRegex = new RegExp( acc + ']' );
+			}
+			if ( ts.characterRegex.test( str ) ) {
+				for ( chr in eq ) {
+					if ( typeof chr === 'string' ) {
+						str = str.replace( ts.characterRegexArray[ chr ], chr );
+					}
+				}
+			}
+			return str;
+		},
+
+		validateOptions : function( c ) {
+			var setting, setting2, typ, timer,
+				// ignore options containing an array
+				ignore = 'headers sortForce sortList sortAppend widgets'.split( ' ' ),
+				orig = c.originalSettings;
+			if ( orig ) {
+				if ( ts.debug(c, 'core') ) {
+					timer = new Date();
+				}
+				for ( setting in orig ) {
+					typ = typeof ts.defaults[setting];
+					if ( typ === 'undefined' ) {
+						console.warn( 'Tablesorter Warning! "table.config.' + setting + '" option not recognized' );
+					} else if ( typ === 'object' ) {
+						for ( setting2 in orig[setting] ) {
+							typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2];
+							if ( $.inArray( setting, ignore ) < 0 && typ === 'undefined' ) {
+								console.warn( 'Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized' );
+							}
+						}
+					}
+				}
+				if ( ts.debug(c, 'core') ) {
+					console.log( 'validate options time:' + ts.benchmark( timer ) );
+				}
+			}
+		},
+
+		// restore headers
+		restoreHeaders : function( table ) {
+			var index, $cell,
+				c = $( table )[ 0 ].config,
+				$headers = c.$table.find( c.selectorHeaders ),
+				len = $headers.length;
+			// don't use c.$headers here in case header cells were swapped
+			for ( index = 0; index < len; index++ ) {
+				$cell = $headers.eq( index );
+				// only restore header cells if it is wrapped
+				// because this is also used by the updateAll method
+				if ( $cell.find( '.' + ts.css.headerIn ).length ) {
+					$cell.html( c.headerContent[ index ] );
+				}
+			}
+		},
+
+		destroy : function( table, removeClasses, callback ) {
+			table = $( table )[ 0 ];
+			if ( !table.hasInitialized ) { return; }
+			// remove all widgets
+			ts.removeWidget( table, true, false );
+			var events,
+				$t = $( table ),
+				c = table.config,
+				$h = $t.find( 'thead:first' ),
+				$r = $h.find( 'tr.' + ts.css.headerRow ).removeClass( ts.css.headerRow + ' ' + c.cssHeaderRow ),
+				$f = $t.find( 'tfoot:first > tr' ).children( 'th, td' );
+			if ( removeClasses === false && $.inArray( 'uitheme', c.widgets ) >= 0 ) {
+				// reapply uitheme classes, in case we want to maintain appearance
+				$t.triggerHandler( 'applyWidgetId', [ 'uitheme' ] );
+				$t.triggerHandler( 'applyWidgetId', [ 'zebra' ] );
+			}
+			// remove widget added rows, just in case
+			$h.find( 'tr' ).not( $r ).remove();
+			// disable tablesorter - not using .unbind( namespace ) because namespacing was
+			// added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/
+			events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' +
+				'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' +
+				'keypress sortBegin sortEnd resetToLoadState '.split( ' ' )
+				.join( c.namespace + ' ' );
+			$t
+				.removeData( 'tablesorter' )
+				.unbind( events.replace( ts.regex.spaces, ' ' ) );
+			c.$headers
+				.add( $f )
+				.removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join( ' ' ) )
+				.removeAttr( 'data-column' )
+				.removeAttr( 'aria-label' )
+				.attr( 'aria-disabled', 'true' );
+			$r
+				.find( c.selectorSort )
+				.unbind( ( 'mousedown mouseup keypress '.split( ' ' ).join( c.namespace + ' ' ) ).replace( ts.regex.spaces, ' ' ) );
+			ts.restoreHeaders( table );
+			$t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false );
+			$t.removeClass(c.namespace.slice(1));
+			// clear flag in case the plugin is initialized again
+			table.hasInitialized = false;
+			delete table.config.cache;
+			if ( typeof callback === 'function' ) {
+				callback( table );
+			}
+			if ( ts.debug(c, 'core') ) {
+				console.log( 'tablesorter has been removed' );
+			}
+		}
+
+	};
+
+	$.fn.tablesorter = function( settings ) {
+		return this.each( function() {
+			var table = this,
+			// merge & extend config options
+			c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods );
+			// save initial settings
+			c.originalSettings = settings;
+			// create a table from data (build table widget)
+			if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) {
+				// return the table (in case the original target is the table's container)
+				ts.buildTable( table, c );
+			} else {
+				ts.setup( table, c );
+			}
+		});
+	};
+
+	// set up debug logs
+	if ( !( window.console && window.console.log ) ) {
+		// access $.tablesorter.logs for browsers that don't have a console...
+		ts.logs = [];
+		/*jshint -W020 */
+		console = {};
+		console.log = console.warn = console.error = console.table = function() {
+			var arg = arguments.length > 1 ? arguments : arguments[0];
+			ts.logs[ ts.logs.length ] = { date: Date.now(), log: arg };
+		};
+	}
+
+	// add default parsers
+	ts.addParser({
+		id : 'no-parser',
+		is : function() {
+			return false;
+		},
+		format : function() {
+			return '';
+		},
+		type : 'text'
+	});
+
+	ts.addParser({
+		id : 'text',
+		is : function() {
+			return true;
+		},
+		format : function( str, table ) {
+			var c = table.config;
+			if ( str ) {
+				str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str );
+				str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str;
+			}
+			return str;
+		},
+		type : 'text'
+	});
+
+	ts.regex.nondigit = /[^\w,. \-()]/g;
+	ts.addParser({
+		id : 'digit',
+		is : function( str ) {
+			return ts.isDigit( str );
+		},
+		format : function( str, table ) {
+			var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table );
+			return str && typeof num === 'number' ? num :
+				str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str;
+		},
+		type : 'numeric'
+	});
+
+	ts.regex.currencyReplace = /[+\-,. ]/g;
+	ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/;
+	ts.addParser({
+		id : 'currency',
+		is : function( str ) {
+			str = ( str || '' ).replace( ts.regex.currencyReplace, '' );
+			// test for £$€¤¥¢
+			return ts.regex.currencyTest.test( str );
+		},
+		format : function( str, table ) {
+			var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table );
+			return str && typeof num === 'number' ? num :
+				str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str;
+		},
+		type : 'numeric'
+	});
+
+	// too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme
+	// now, this regex can be updated before initialization
+	ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//;
+	ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/;
+	ts.addParser({
+		id : 'url',
+		is : function( str ) {
+			return ts.regex.urlProtocolTest.test( str );
+		},
+		format : function( str ) {
+			return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str;
+		},
+		type : 'text'
+	});
+
+	ts.regex.dash = /-/g;
+	ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/;
+	ts.addParser({
+		id : 'isoDate',
+		is : function( str ) {
+			return ts.regex.isoDate.test( str );
+		},
+		format : function( str ) {
+			var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str;
+			return date instanceof Date && isFinite( date ) ? date.getTime() : str;
+		},
+		type : 'numeric'
+	});
+
+	ts.regex.percent = /%/g;
+	ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/;
+	ts.addParser({
+		id : 'percent',
+		is : function( str ) {
+			return ts.regex.percentTest.test( str ) && str.length < 15;
+		},
+		format : function( str, table ) {
+			return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str;
+		},
+		type : 'numeric'
+	});
+
+	// added image parser to core v2.17.9
+	ts.addParser({
+		id : 'image',
+		is : function( str, table, node, $node ) {
+			return $node.find( 'img' ).length > 0;
+		},
+		format : function( str, table, cell ) {
+			return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str;
+		},
+		parsed : true, // filter widget flag
+		type : 'text'
+	});
+
+	ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser
+	ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i;
+	ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i;
+	ts.addParser({
+		id : 'usLongDate',
+		is : function( str ) {
+			// two digit years are not allowed cross-browser
+			// Jan 01, 2013 12:34:56 PM or 01 Jan 2013
+			return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str );
+		},
+		format : function( str ) {
+			var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str;
+			return date instanceof Date && isFinite( date ) ? date.getTime() : str;
+		},
+		type : 'numeric'
+	});
+
+	// testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
+	ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/;
+	// escaped "-" because JSHint in Firefox was showing it as an error
+	ts.regex.shortDateReplace = /[\-.,]/g;
+	// XXY covers MDY & DMY formats
+	ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/;
+	ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/;
+	ts.convertFormat = function( dateString, format ) {
+		dateString = ( dateString || '' )
+			.replace( ts.regex.spaces, ' ' )
+			.replace( ts.regex.shortDateReplace, '/' );
+		if ( format === 'mmddyyyy' ) {
+			dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' );
+		} else if ( format === 'ddmmyyyy' ) {
+			dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' );
+		} else if ( format === 'yyyymmdd' ) {
+			dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' );
+		}
+		var date = new Date( dateString );
+		return date instanceof Date && isFinite( date ) ? date.getTime() : '';
+	};
+
+	ts.addParser({
+		id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
+		is : function( str ) {
+			str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' );
+			return ts.regex.shortDateTest.test( str );
+		},
+		format : function( str, table, cell, cellIndex ) {
+			if ( str ) {
+				var c = table.config,
+					$header = c.$headerIndexed[ cellIndex ],
+					format = $header.length && $header.data( 'dateFormat' ) ||
+						ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) ||
+						c.dateFormat;
+				// save format because getData can be slow...
+				if ( $header.length ) {
+					$header.data( 'dateFormat', format );
+				}
+				return ts.convertFormat( str, format ) || str;
+			}
+			return str;
+		},
+		type : 'numeric'
+	});
+
+	// match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk
+	ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i;
+	ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i;
+	ts.addParser({
+		id : 'time',
+		is : function( str ) {
+			return ts.regex.timeTest.test( str );
+		},
+		format : function( str ) {
+			// isolate time... ignore month, day and year
+			var temp,
+				timePart = ( str || '' ).match( ts.regex.timeMatch ),
+				orig = new Date( str ),
+				// no time component? default to 00:00 by leaving it out, but only if str is defined
+				time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ),
+				date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time;
+			if ( date instanceof Date && isFinite( date ) ) {
+				temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0;
+				// if original string was a valid date, add it to the decimal so the column sorts in some kind of order
+				// luckily new Date() ignores the decimals
+				return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime();
+			}
+			return str;
+		},
+		type : 'numeric'
+	});
+
+	ts.addParser({
+		id : 'metadata',
+		is : function() {
+			return false;
+		},
+		format : function( str, table, cell ) {
+			var c = table.config,
+			p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName;
+			return $( cell ).metadata()[ p ];
+		},
+		type : 'numeric'
+	});
+
+	/*
+		██████ ██████ █████▄ █████▄ ▄████▄
+		  ▄█▀  ██▄▄   ██▄▄██ ██▄▄██ ██▄▄██
+		▄█▀    ██▀▀   ██▀▀██ ██▀▀█  ██▀▀██
+		██████ ██████ █████▀ ██  ██ ██  ██
+		*/
+	// add default widgets
+	ts.addWidget({
+		id : 'zebra',
+		priority : 90,
+		format : function( table, c, wo ) {
+			var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len,
+				child = new RegExp( c.cssChildRow, 'i' ),
+				$tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) );
+			for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
+				// loop through the visible rows
+				count = 0;
+				$visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove );
+				len = $visibleRows.length;
+				for ( rowIndex = 0; rowIndex < len; rowIndex++ ) {
+					$row = $visibleRows.eq( rowIndex );
+					// style child rows the same way the parent row was styled
+					if ( !child.test( $row[ 0 ].className ) ) { count++; }
+					isEven = ( count % 2 === 0 );
+					$row
+						.removeClass( wo.zebra[ isEven ? 1 : 0 ] )
+						.addClass( wo.zebra[ isEven ? 0 : 1 ] );
+				}
+			}
+		},
+		remove : function( table, c, wo, refreshing ) {
+			if ( refreshing ) { return; }
+			var tbodyIndex, $tbody,
+				$tbodies = c.$tbodies,
+				toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' );
+			for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
+				$tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody
+				$tbody.children().removeClass( toRemove );
+				ts.processTbody( table, $tbody, false ); // restore tbody
+			}
+		}
+	});
+
+})( jQuery );
+return jQuery.tablesorter;}));
diff --git a/Allura/allura/public/nf/js/sylvester.js b/Allura/allura/public/nf/js/sylvester.js
index 165221e17..43c34a71d 100755
--- a/Allura/allura/public/nf/js/sylvester.js
+++ b/Allura/allura/public/nf/js/sylvester.js
@@ -19,4 +19,4 @@
 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
-eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('9 17={3i:\'0.1.3\',16:1e-6};l v(){}v.23={e:l(i){8(i<1||i>7.4.q)?w:7.4[i-1]},2R:l(){8 7.4.q},1u:l(){8 F.1x(7.2u(7))},24:l(a){9 n=7.4.q;9 V=a.4||a;o(n!=V.q){8 1L}J{o(F. [...]
\ No newline at end of file
+var Sylvester={version:"0.1.3",precision:1e-6};function Vector(){}function Matrix(){}function Line(){}function Plane(){}Vector.prototype={e:function(e){return e<1||e>this.elements.length?null:this.elements[e-1]},dimensions:function(){return this.elements.length},modulus:function(){return Math.sqrt(this.dot(this))},eql:function(e){var t=this.elements.length,n=e.elements||e;if(t!=n.length)return!1;do{if(Math.abs(this.elements[t-1]-n[t-1])>Sylvester.precision)return!1}while(--t);return!0},d [...]
\ No newline at end of file
diff --git a/Allura/allura/templates/site_admin_new_projects.html b/Allura/allura/templates/site_admin_new_projects.html
index eb2d931f5..55da3fdf2 100644
--- a/Allura/allura/templates/site_admin_new_projects.html
+++ b/Allura/allura/templates/site_admin_new_projects.html
@@ -112,5 +112,13 @@
 {% block extra_js %}
   <script type="text/javascript" src="{{g.forge_static('js/site_admin_new_projects.js')}}"></script>
   <script type="text/javascript" src="{{g.forge_static('js/jquery.tablesorter.js')}}"></script>
-  <script type="text/JavaScript">$(document).ready(function() {$("#new_projects").tablesorter({headers: {0: {sorter: false}}});} );</script>
+  <script type="text/JavaScript">$(document).ready(function() {
+      $(function(){
+		$('#new_projects').tablesorter({
+			usNumberFormat : false,
+			sortReset      : true,
+			sortRestart    : true
+		});
+	});
+  } );</script>
 {% endblock %}