diff --git a/js/longtable/longtable.js b/js/longtable/longtable.js
new file mode 100644
index 00000000..fe3ad1c6
--- /dev/null
+++ b/js/longtable/longtable.js
@@ -0,0 +1,184 @@
+$.fn.longtable = function(fields, options, data) {
+ var elem = $(this).addClass("longtable");
+
+ var orig_data = data;
+
+ options.row_h = options.row_h || 22;
+ options.checkbox = options.checkbox || false;
+
+ var shown_rows = {};
+
+ var sorted_by = undefined;
+ var sorted_reverse = false;
+
+ var filter = function() { return true; };
+
+ var lt = {
+ _gen_field_td: function(field, id) {
+ var el;
+ if (id === undefined) {
+ el = $("
| ");
+ el.html(fields[field].name || field);
+
+ if (!fields[field].sort_disable) {
+ el.addClass("sortable").click(function() {
+ lt._sort_by(field);
+ });
+ }
+ }
+ else {
+ el = $(" | ");
+ if (fields[field].fmt) { // Special formatting?
+ el.html(fields[field].fmt(data[id]));
+ }
+ else {
+ el.html(data[id][field]);
+ }
+ }
+ el.css("width", fields[field].width);
+ el.css("height", options.row_h);
+ return el;
+ },
+ _gen_tr: function(id) {
+ var el = $("
");
+ $.each(fields, function(field, f) {
+ lt._gen_field_td(field, id).appendTo(el);
+ });
+ if (id !== undefined) {
+ el.addClass("row").addClass("row_"+id);
+ el.css({position: "absolute", top: options.row_h * (id+1)});
+ }
+ return el;
+ },
+ _clean: function() {
+ elem.find('.row').remove();
+ shown_rows = {};
+ },
+ _remove: function(id) {
+ elem.find('.row_'+id).remove();
+ delete shown_rows[id];
+ },
+ _insert: function(id) {
+ var el = lt._gen_tr(id).appendTo(elem);
+ $(elem).trigger("new-row", [data[id], el]);
+ shown_rows[id] = true;
+ },
+
+ _sort_by: function(field) {
+ if (field !== undefined) {
+ if (sorted_by == field) {
+ sorted_reverse = !sorted_reverse;
+ }
+ else {
+ sorted_reverse = !!fields[field].sort_reverse;
+ sorted_by = field;
+ }
+ }
+ lt.sort_by(sorted_by, sorted_reverse);
+ },
+
+ _apply_filter: function() {
+ data = data.filter(filter);
+ },
+ _reset_data: function() {
+ data = orig_data;
+ },
+
+
+ set_filter: function(f) {
+ filter = f;
+ lt._reset_data();
+ lt._apply_filter();
+ lt._sort_by();
+ },
+
+ sort_by: function(field, reverse) {
+ if (field !== undefined) {
+ sorted_by = field;
+ sorted_reverse = reverse;
+
+ var ord = fields[field].sort_fun || function(a,b) { return lt.sort_alphanum(a[field], b[field]); };
+
+ data = data.sort(ord);
+ if (reverse) data = data.reverse();
+ }
+
+ lt.update_data();
+ },
+
+ update_viewport: function() {
+ var first = $(window).scrollTop() - $(elem).offset().top - options.row_h;
+ var last = first + $(window).height();
+
+ first = Math.floor(first / options.row_h);
+ last = Math.ceil (last / options.row_h);
+
+ first = first < 0 ? 0 : first;
+ last = last >= data.length ? data.length - 1 : last;
+
+ $.each(shown_rows, function(id) {
+ if (id < first || id > last) {
+ lt._remove(id);
+ }
+ });
+
+ for (var id = first; id <= last; id++) {
+ if (!shown_rows[id]) lt._insert(id);
+ }
+ },
+
+ update_data: function() {
+ $(elem).height((data.length + 1) * options.row_h);
+
+ lt._clean();
+ lt.update_viewport();
+ },
+
+ get_data: function() {
+ return data;
+ },
+
+ destroy: function() {
+ },
+
+ // http://web.archive.org/web/20130826203933/http://my.opera.com/GreyWyvern/blog/show.dml/1671288
+ sort_alphanum: function(a, b) {
+ function chunkify(t) {
+ var tz = [], x = 0, y = -1, n = 0, i, j;
+
+ while (i = (j = t.charAt(x++)).charCodeAt(0)) {
+ var m = (/* dot: i == 46 || */(i >=48 && i <= 57));
+ if (m !== n) {
+ tz[++y] = "";
+ n = m;
+ }
+ tz[y] += j;
+ }
+ return tz;
+ }
+
+ var aa = chunkify((""+a).toLowerCase());
+ var bb = chunkify((""+b).toLowerCase());
+
+ for (x = 0; aa[x] && bb[x]; x++) {
+ if (aa[x] !== bb[x]) {
+ var c = Number(aa[x]), d = Number(bb[x]);
+ if (c == aa[x] && d == bb[x]) {
+ return c - d;
+ } else return (aa[x] > bb[x]) ? 1 : -1;
+ }
+ }
+ return aa.length - bb.length;
+ }
+ // End of foreign code
+ };
+
+
+
+ lt._gen_tr().appendTo(elem);
+ lt.update_data();
+
+ $(window).on("scroll resize", lt.update_viewport);
+
+ return lt;
+};
diff --git a/stylesheets/longtable/longtable.css b/stylesheets/longtable/longtable.css
new file mode 100644
index 00000000..09d9a0ab
--- /dev/null
+++ b/stylesheets/longtable/longtable.css
@@ -0,0 +1,31 @@
+.longtable {
+ display: block;
+ position: relative;
+ box-sizing: border-box;
+}
+.longtable > tbody {
+ display: block;
+ box-sizing: border-box;
+}
+.longtable > tbody > tr {
+ display: block;
+ box-sizing: border-box;
+ clear: left;
+ width: 100%;
+}
+.longtable > tbody > tr > td {
+ display: block;
+ box-sizing: border-box;
+ float: left;
+ padding: 0;
+}
+.longtable > tbody > tr > th {
+ display: block;
+ box-sizing: border-box;
+ float: left;
+ padding: 0;
+}
+.longtable > tbody > tr > th.sortable {
+ cursor: pointer;
+}
+