// Stupid jQuery 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);