Stupidtable.js

Note: After saving, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
// Stupid jQuery table plugin. (http://joequery.github.io/Stupid-Table-Plugin/)

(function($) {
  $.fn.stupidtable = function(sortFns) {
    return this.each(function() {
      var $table = $(this);
      sortFns = sortFns || {};
      sortFns = $.extend({}, $.fn.stupidtable.default_sort_fns, sortFns);
      $table.data('sortFns', sortFns);
      $table.stupidtable_build();

      $table.on("click.stupidtable", "thead th", function() {
          $(this).stupidsort();
      });

      // Sort th immediately if data-sort-onload="yes" is specified. Limit to
      // the first one found - only one default sort column makes sense anyway.
      var $th_onload_sort = $table.find("th[data-sort-onload=yes]").eq(0);
      $th_onload_sort.stupidsort();
    });
  };

  // ------------------------------------------------------------------
  // Default settings
  // ------------------------------------------------------------------
  $.fn.stupidtable.default_settings = {
    should_redraw: function(sort_info){
      return true;
    },
    will_manually_build_table: false
  };
  $.fn.stupidtable.dir = {ASC: "asc", DESC: "desc"};
  $.fn.stupidtable.default_sort_fns = {
    "int": function(a, b) {
      return parseInt(a, 10) - parseInt(b, 10);
    },
    "float": function(a, b) {
      return parseFloat(a) - parseFloat(b);
    },
    "string": function(a, b) {
      return a.toString().localeCompare(b.toString());
    },
    "string-ins": function(a, b) {
      a = a.toString().toLocaleLowerCase();
      b = b.toString().toLocaleLowerCase();
      return a.localeCompare(b);
    }
  };

  // Allow specification of settings on a per-table basis. Call on a table
  // jquery object. Call *before* calling .stuidtable();
  $.fn.stupidtable_settings = function(settings) {
    return this.each(function() {
      var $table = $(this);
      var final_settings = $.extend({}, $.fn.stupidtable.default_settings, settings);
      $table.stupidtable.settings = final_settings;
    });
  };


  // Expects $("#mytable").stupidtable() to have already been called.
  // Call on a table header.
  $.fn.stupidsort = function(force_direction){
    var $this_th = $(this);
    var datatype = $this_th.data("sort") || null;

    // No datatype? Nothing to do.
    if (datatype === null) {
      return;
    }

    var $table = $this_th.closest("table");

    var sort_info = {
        $th: $this_th,
        $table: $table,
        datatype: datatype
    };


    // Bring in default settings if none provided
    if(!$table.stupidtable.settings){
        $table.stupidtable.settings = $.extend({}, $.fn.stupidtable.default_settings);
    }

    sort_info.compare_fn = $table.data('sortFns')[datatype];
    sort_info.th_index = calculateTHIndex(sort_info);
    sort_info.sort_dir = calculateSortDir(force_direction, sort_info);

    $this_th.data("sort-dir", sort_info.sort_dir);
    $table.trigger("beforetablesort", {column: sort_info.th_index, direction: sort_info.sort_dir, $th: $this_th});

    // More reliable method of forcing a redraw
    $table.css("display");

    // Run sorting asynchronously on a timout to force browser redraw after
    // `beforetablesort` callback. Also avoids locking up the browser too much.
    setTimeout(function() {
      if(!$table.stupidtable.settings.will_manually_build_table){
        $table.stupidtable_build();
      }
      var table_structure = sortTable(sort_info);
      var trs = getTableRowsFromTableStructure(table_structure, sort_info);

      if(!$table.stupidtable.settings.should_redraw(sort_info)){
        return;
      }
      $table.children("tbody").append(trs);

      updateElementData(sort_info);
      $table.trigger("aftertablesort", {column: sort_info.th_index, direction: sort_info.sort_dir, $th: $this_th});
      $table.css("display");

    }, 10);
    return $this_th;
  };

  // Call on a sortable td to update its value in the sort. This should be the
  // only mechanism used to update a cell's sort value. If your display value is
  // different from your sort value, use jQuery's .text() or .html() to update
  // the td contents, Assumes stupidtable has already been called for the table.
  $.fn.updateSortVal = function(new_sort_val){
  var $this_td = $(this);
    if($this_td.is('[data-sort-value]')){
      // For visual consistency with the .data cache
      $this_td.attr('data-sort-value', new_sort_val);
    }
    $this_td.data("sort-value", new_sort_val);
    return $this_td;
  };


  $.fn.stupidtable_build = function(){
    return this.each(function() {
      var $table = $(this);
      var table_structure = [];
      var trs = $table.children("tbody").children("tr");
      trs.each(function(index,tr) {

        // ====================================================================
        // Transfer to using internal table structure
        // ====================================================================
        var ele = {
            $tr: $(tr),
            columns: [],
            index: index
        };

        $(tr).children('td').each(function(idx, td){
            var sort_val = $(td).data("sort-value");

            // Store and read from the .data cache for display text only sorts
            // instead of looking through the DOM every time
            if(typeof(sort_val) === "undefined"){
              var txt = $(td).text();
              $(td).data('sort-value', txt);
              sort_val = txt;
            }
            ele.columns.push(sort_val);
        });
        table_structure.push(ele);
      });
      $table.data('stupidsort_internaltable', table_structure);
    });
  };

  // ====================================================================
  // Private functions
  // ====================================================================
  var sortTable = function(sort_info){
    var table_structure = sort_info.$table.data('stupidsort_internaltable');
    var th_index = sort_info.th_index;
    var $th = sort_info.$th;

    var multicolumn_target_str = $th.data('sort-multicolumn');
    var multicolumn_targets;
    if(multicolumn_target_str){
        multicolumn_targets = multicolumn_target_str.split(',');
    }
    else{
        multicolumn_targets = [];
    }
    var multicolumn_th_targets = $.map(multicolumn_targets, function(identifier, i){
        return get_th(sort_info.$table, identifier);
    });

    table_structure.sort(function(e1, e2){
      var multicolumns = multicolumn_th_targets.slice(0); // shallow copy
      var diff = sort_info.compare_fn(e1.columns[th_index], e2.columns[th_index]);
      while(diff === 0 && multicolumns.length){
          var multicolumn = multicolumns[0];
          var datatype = multicolumn.$e.data("sort");
          var multiCloumnSortMethod = sort_info.$table.data('sortFns')[datatype];
          diff = multiCloumnSortMethod(e1.columns[multicolumn.index], e2.columns[multicolumn.index]);
          multicolumns.shift();
      }
      // Sort by position in the table if values are the same. This enforces a
      // stable sort across all browsers. See https://bugs.chromium.org/p/v8/issues/detail?id=90
      if (diff === 0)
        return e1.index - e2.index;
      else
        return diff;

    });

    if (sort_info.sort_dir != $.fn.stupidtable.dir.ASC){
      table_structure.reverse();
    }
      return table_structure;
  };

  var get_th = function($table, identifier){
      // identifier can be a th id or a th index number;
      var $table_ths = $table.find('th');
      var index = parseInt(identifier, 10);
      var $th;
      if(!index && index !== 0){
          $th = $table_ths.siblings('#' + identifier);
          index = $table_ths.index($th);
      }
      else{
          $th = $table_ths.eq(index);
      }
      return {index: index, $e: $th};
  };

  var getTableRowsFromTableStructure = function(table_structure, sort_info){
    // Gather individual column for callbacks
    var column = $.map(table_structure, function(ele, i){
        return [[ele.columns[sort_info.th_index], ele.$tr, i]];
    });

    /* Side effect */
    sort_info.column = column;

    // Replace the content of tbody with the sorted rows. Strangely
    // enough, .append accomplishes this for us.
    return $.map(table_structure, function(ele) { return ele.$tr; });

  };

  var updateElementData = function(sort_info){
    var $table = sort_info.$table;
    var $this_th = sort_info.$th;
    var sort_dir = $this_th.data('sort-dir');


    // Reset siblings
    $table.find("th").data("sort-dir", null).removeClass("sorting-desc sorting-asc");
    $this_th.data("sort-dir", sort_dir).addClass("sorting-"+sort_dir);
  };

  var calculateSortDir = function(force_direction, sort_info){
    var sort_dir;
    var $this_th = sort_info.$th;
    var dir = $.fn.stupidtable.dir;

    if(force_direction){
        sort_dir = force_direction;
    }
    else{
        sort_dir = force_direction || $this_th.data("sort-default") || dir.ASC;
        if ($this_th.data("sort-dir"))
           sort_dir = $this_th.data("sort-dir") === dir.ASC ? dir.DESC : dir.ASC;
    }
    return sort_dir;
  };

  var calculateTHIndex = function(sort_info){
    var th_index = 0;
    var base_index = sort_info.$th.index();
    sort_info.$th.parents("tr").find("th").slice(0, base_index).each(function() {
      var cols = $(this).attr("colspan") || 1;
      th_index += parseInt(cols,10);
    });
    return th_index;
  };

})(window.jQuery);