Source: tablesorter.js

/***
 * The only plugin's dependency - jQuery. It is required before
 * creating the plugin.
 */
var jQuery = require("jquery");

/**
 * There is an IIFE around the plugin.
 * The IIFE contains several helper functions.
 * The plugin gets assigned to `$.fn.tablesorter`.
 * @namespace TablesorterIIFE
 * @author Curveball <x.curveball.x@gmail.com>
 * @license MIT
 *
 */
(function($, window, document, undefined) {
    "use strict";
    $.fn._init = $.fn.init;

    /**
     * Makes possible to get selector string later by attaching it to the jQuery object.
     * Wrapper around original init() function.
     * @function TablesorterIIFE~init
     * @param {string|*} selector selector string
     * @param {object} context an object serving as context for selectors search. The match will be searched for in its children instead of the entire page.
     * @param {object} root usually $(document).
     * @returns {object} a jQuery object
     */
    $.fn.init = function(selector, context, root) {
        return (typeof selector === 'string') ? new $.fn._init(selector, context, root).data('selector', selector) : new $.fn._init(selector, context, root);
    };

    /**
     * Gets selector string passed to the plugin.
     * @function TablesorterIIFE~getSelector
     * @returns {string} selector string passed to the plugin.
     */
    $.fn.getSelector = function() {
        return $(this).data('selector');
    };

    /**
     * The plugin function assigned to `$.fn.tablesorter`.
     * Below go inner helper functions inside the plugin.
     * @namespace Tablesorter
     * @author Curveball <x.curveball.x@gmail.com>
     * @license MIT
     * @param {object.<string, *>} [options] object passed to the plugin upon calling
     * @returns {object} returns collection of matched elements.
     */
    $.fn.tablesorter = function(options) {

        var settings = $.extend({}, $.fn.tablesorter.settings, options);

        var selector = $(this).getSelector(),
            ths = " ." + settings.tablesorterTitlesClass,
            trs = " ." + settings.tablesorterGroupsClass,
            cells = $(selector + ths)[0].cells,
            columns = settings.tablesorterColumns,
            curIndex = null,
            curType = null,
            curOrder = null,
            curElem = null,
            busy = false;

        /**
         * Handles clicks on columns' headers. Does some checks and invokes row sorting.
         * @function Tablesorter~tablesorter
         * @param {MouseEvent} event event object corresponding to click on controls
         * @returns {undefined}
         */
        function tablesorter(event) {
            if(busy || !$(event.target).hasClass('sortable')) return;
            toggleBusyFlag();
            curElem = $(event.target);
            curOrder = curElem.data('sortOrder');
            curIndex = getColIndex();
            curType = getValuesType();

            if(curIndex === null || (curType !== 'number' && curType !== 'string')) return;

            curElem.removeClass(curOrder === 'asc' ? 'desc' : 'asc').addClass(curOrder);

            sortRows();
            toggleSortingOrderForCol();
            toggleBusyFlag();
        }

        /**
         * Toggles plugin's busy flag.
         * @function Tablesorter~toggleBusyFlag
         * @returns {undefined}
         */
        function toggleBusyFlag() {
            busy = busy === true ? false : true;
        }

        /**
         * Toggles column's sorting order flag.
         * @function Tablesorter~toggleSortingOrderForCol
         * @returns {undefined}
         */
        function toggleSortingOrderForCol() {
            if(curElem.data('sortOrder') === 'desc') {
                curElem.data('sortOrder', 'asc');
            } else {
                curElem.data('sortOrder', 'desc');
            }
        }

        /**
         * Sorts table rows.
         * @function Tablesorter~sortRows
         * @returns {undefined}
         */
        function sortRows() {
            var rowsBlocks = $(selector + trs);
            $.each(rowsBlocks, function(index, rowsBlock) {
                var rows = $(rowsBlock).find("tr");
                if(curType === 'number') {
                    rows.sort(sortAsNumbers);
                } else {
                    rows.sort(sortAsStrings);
                }
                $(rowsBlock).empty().append(rows);
            });
        }

        /**
         * Sorting function. Compares two rows' cells with numeric content.
         * @function Tablesorter~sortAsNumbers
         * @param {HTMLTableRowElement} rowA one row object
         * @param {HTMLTableRowElement} rowB another row object
         * @returns {number} number, depending on what value is greater given the sorting order.
         */
        function sortAsNumbers(rowA, rowB) {
            var valA = parseFloat(rowA.cells[curIndex].textContent),
                valB = parseFloat(rowB.cells[curIndex].textContent);
            if(curOrder === 'asc')  return (valA > valB) ? 1 : (valA < valB) ? -1 : rowA.sectionRowIndex - rowB.sectionRowIndex;
            if(curOrder === 'desc') return (valB > valA) ? 1 : (valB < valA) ? -1 : rowA.sectionRowIndex - rowB.sectionRowIndex;
        }

        /**
         * Sorting function. Compares two rows' cells with textual content.
         * @function Tablesorter~sortAsStrings
         * @param {HTMLTableRowElement} rowA one row object
         * @param {HTMLTableRowElement} rowB another row object
         * @returns {number} number, depending on what value is greater given the sorting order.
         */
        function sortAsStrings(rowA, rowB) {
            var valA = rowA.cells[curIndex].textContent,
                valB = rowB.cells[curIndex].textContent;
            if(curOrder === 'asc')  return (valA > valB) ? 1 : (valA < valB) ? -1 : rowA.sectionRowIndex - rowB.sectionRowIndex;
            if(curOrder === 'desc') return (valB > valA) ? 1 : (valB < valA) ? -1 : rowA.sectionRowIndex - rowB.sectionRowIndex;
        }

        /**
         * Gets column values' datatype.
         * @function Tablesorter~getValuesType
         * @returns {string} column values' datatype.
         */
        function getValuesType() {
            return $.isNumeric($(selector + trs + " tr")[0].cells[curIndex].textContent) ? 'number' : 'string';
        }

        /**
         * Gets column's index.
         * @function Tablesorter~getColIndex
         * @returns {number} column's index.
         */
        function getColIndex() {
            var allths = curElem[0].parentElement.cells, colIndex = null;
            $.each(allths, function(index, cell) {
                if(curElem[0] === cell) colIndex = index;
                $(cell).removeClass( "desc asc" );
            });
            return colIndex;
        }

        (function() {
            if(columns.length === 0) return;
            for(var i = 0; i < columns.length; i++) {
                $(cells[columns[i].col]).addClass('sortable').data('sortOrder', columns[i].order);
            }
            $("body").on( "click", selector + ths, function(event) {
                tablesorter(event);
            });
        })();

        return this;
    };

    /*** Plugin's default settings*/
    $.fn.tablesorter.settings = {
        tablesorterTitlesClass: 'tsTitles',
        tablesorterGroupsClass: 'tsGroup',
        tablesorterColumns: []
    };

})(jQuery, window, document);