0
|
1 /*
|
|
2 * tablesaw: A set of plugins for responsive tables
|
|
3 * Sortable column headers
|
|
4 * Copyright (c) 2013 Filament Group, Inc.
|
|
5 * MIT License
|
|
6 */
|
|
7
|
|
8 (function() {
|
|
9 function getSortValue(cell) {
|
|
10 var text = [];
|
|
11 $(cell.childNodes).each(function() {
|
|
12 var $el = $(this);
|
|
13 if ($el.is("input, select")) {
|
|
14 text.push($el.val());
|
|
15 } else if ($el.is(".tablesaw-cell-label")) {
|
|
16 } else {
|
|
17 text.push(($el.text() || "").replace(/^\s+|\s+$/g, ""));
|
|
18 }
|
|
19 });
|
|
20
|
|
21 return text.join("");
|
|
22 }
|
|
23
|
|
24 var pluginName = "tablesaw-sortable",
|
|
25 initSelector = "table[data-" + pluginName + "]",
|
|
26 sortableSwitchSelector = "[data-" + pluginName + "-switch]",
|
|
27 attrs = {
|
|
28 sortCol: "data-tablesaw-sortable-col",
|
|
29 defaultCol: "data-tablesaw-sortable-default-col",
|
|
30 numericCol: "data-tablesaw-sortable-numeric",
|
|
31 subRow: "data-tablesaw-subrow",
|
|
32 ignoreRow: "data-tablesaw-ignorerow"
|
|
33 },
|
|
34 classes = {
|
|
35 head: pluginName + "-head",
|
|
36 ascend: pluginName + "-ascending",
|
|
37 descend: pluginName + "-descending",
|
|
38 switcher: pluginName + "-switch",
|
|
39 tableToolbar: "tablesaw-bar-section",
|
|
40 sortButton: pluginName + "-btn"
|
|
41 },
|
|
42 methods = {
|
|
43 _create: function(o) {
|
|
44 return $(this).each(function() {
|
|
45 var init = $(this).data(pluginName + "-init");
|
|
46 if (init) {
|
|
47 return false;
|
|
48 }
|
|
49 $(this)
|
|
50 .data(pluginName + "-init", true)
|
|
51 .trigger("beforecreate." + pluginName)
|
|
52 [pluginName]("_init", o)
|
|
53 .trigger("create." + pluginName);
|
|
54 });
|
|
55 },
|
|
56 _init: function() {
|
|
57 var el = $(this);
|
|
58 var tblsaw = el.data("tablesaw");
|
|
59 var heads;
|
|
60 var $switcher;
|
|
61
|
|
62 function addClassToHeads(h) {
|
|
63 $.each(h, function(i, v) {
|
|
64 $(v).addClass(classes.head);
|
|
65 });
|
|
66 }
|
|
67
|
|
68 function makeHeadsActionable(h, fn) {
|
|
69 $.each(h, function(i, col) {
|
|
70 var b = $("<button class='" + classes.sortButton + "'/>");
|
|
71 b.on("click", { col: col }, fn);
|
|
72 $(col)
|
|
73 .wrapInner(b)
|
|
74 .find("button")
|
|
75 .append("<span class='tablesaw-sortable-arrow'>");
|
|
76 });
|
|
77 }
|
|
78
|
|
79 function clearOthers(headcells) {
|
|
80 $.each(headcells, function(i, v) {
|
|
81 var col = $(v);
|
|
82 col.removeAttr(attrs.defaultCol);
|
|
83 col.removeClass(classes.ascend);
|
|
84 col.removeClass(classes.descend);
|
|
85 });
|
|
86 }
|
|
87
|
|
88 function headsOnAction(e) {
|
|
89 if ($(e.target).is("a[href]")) {
|
|
90 return;
|
|
91 }
|
|
92
|
|
93 e.stopPropagation();
|
|
94 var headCell = $(e.target).closest("[" + attrs.sortCol + "]"),
|
|
95 v = e.data.col,
|
|
96 newSortValue = heads.index(headCell[0]);
|
|
97
|
|
98 clearOthers(
|
|
99 headCell
|
|
100 .closest("thead")
|
|
101 .find("th")
|
|
102 .filter(function() {
|
|
103 return this !== headCell[0];
|
|
104 })
|
|
105 );
|
|
106 if (headCell.is("." + classes.descend) || !headCell.is("." + classes.ascend)) {
|
|
107 el[pluginName]("sortBy", v, true);
|
|
108 newSortValue += "_asc";
|
|
109 } else {
|
|
110 el[pluginName]("sortBy", v);
|
|
111 newSortValue += "_desc";
|
|
112 }
|
|
113 if ($switcher) {
|
|
114 $switcher
|
|
115 .find("select")
|
|
116 .val(newSortValue)
|
|
117 .trigger("refresh");
|
|
118 }
|
|
119
|
|
120 e.preventDefault();
|
|
121 }
|
|
122
|
|
123 function handleDefault(heads) {
|
|
124 $.each(heads, function(idx, el) {
|
|
125 var $el = $(el);
|
|
126 if ($el.is("[" + attrs.defaultCol + "]")) {
|
|
127 if (!$el.is("." + classes.descend)) {
|
|
128 $el.addClass(classes.ascend);
|
|
129 }
|
|
130 }
|
|
131 });
|
|
132 }
|
|
133
|
|
134 function addSwitcher(heads) {
|
|
135 $switcher = $("<div>")
|
|
136 .addClass(classes.switcher)
|
|
137 .addClass(classes.tableToolbar);
|
|
138
|
|
139 var html = ["<label>" + Tablesaw.i18n.sort + ":"];
|
|
140
|
|
141 // TODO next major version: remove .btn
|
|
142 html.push('<span class="btn tablesaw-btn"><select>');
|
|
143 heads.each(function(j) {
|
|
144 var $t = $(this);
|
|
145 var isDefaultCol = $t.is("[" + attrs.defaultCol + "]");
|
|
146 var isDescending = $t.is("." + classes.descend);
|
|
147
|
|
148 var hasNumericAttribute = $t.is("[" + attrs.numericCol + "]");
|
|
149 var numericCount = 0;
|
|
150 // Check only the first four rows to see if the column is numbers.
|
|
151 var numericCountMax = 5;
|
|
152
|
|
153 $(this.cells.slice(0, numericCountMax)).each(function() {
|
|
154 if (!isNaN(parseInt(getSortValue(this), 10))) {
|
|
155 numericCount++;
|
|
156 }
|
|
157 });
|
|
158 var isNumeric = numericCount === numericCountMax;
|
|
159 if (!hasNumericAttribute) {
|
|
160 $t.attr(attrs.numericCol, isNumeric ? "" : "false");
|
|
161 }
|
|
162
|
|
163 html.push(
|
|
164 "<option" +
|
|
165 (isDefaultCol && !isDescending ? " selected" : "") +
|
|
166 ' value="' +
|
|
167 j +
|
|
168 '_asc">' +
|
|
169 $t.text() +
|
|
170 " " +
|
|
171 (isNumeric ? "↑" : "(A-Z)") +
|
|
172 "</option>"
|
|
173 );
|
|
174 html.push(
|
|
175 "<option" +
|
|
176 (isDefaultCol && isDescending ? " selected" : "") +
|
|
177 ' value="' +
|
|
178 j +
|
|
179 '_desc">' +
|
|
180 $t.text() +
|
|
181 " " +
|
|
182 (isNumeric ? "↓" : "(Z-A)") +
|
|
183 "</option>"
|
|
184 );
|
|
185 });
|
|
186 html.push("</select></span></label>");
|
|
187
|
|
188 $switcher.html(html.join(""));
|
|
189
|
|
190 var $firstChild = tblsaw.$toolbar.children().eq(0);
|
|
191 if ($firstChild.length) {
|
|
192 $switcher.insertBefore($firstChild);
|
|
193 } else {
|
|
194 $switcher.appendTo(tblsaw.$toolbar);
|
|
195 }
|
|
196 $switcher.find(".tablesaw-btn").tablesawbtn();
|
|
197 $switcher.find("select").on("change", function() {
|
|
198 var val = $(this)
|
|
199 .val()
|
|
200 .split("_"),
|
|
201 head = heads.eq(val[0]);
|
|
202
|
|
203 clearOthers(head.siblings());
|
|
204 el[pluginName]("sortBy", head.get(0), val[1] === "asc");
|
|
205 });
|
|
206 }
|
|
207
|
|
208 el.addClass(pluginName);
|
|
209
|
|
210 heads = el
|
|
211 .children()
|
|
212 .filter("thead")
|
|
213 .find("th[" + attrs.sortCol + "]");
|
|
214
|
|
215 addClassToHeads(heads);
|
|
216 makeHeadsActionable(heads, headsOnAction);
|
|
217 handleDefault(heads);
|
|
218
|
|
219 if (el.is(sortableSwitchSelector)) {
|
|
220 addSwitcher(heads);
|
|
221 }
|
|
222 },
|
|
223 sortRows: function(rows, colNum, ascending, col, tbody) {
|
|
224 function convertCells(cellArr, belongingToTbody) {
|
|
225 var cells = [];
|
|
226 $.each(cellArr, function(i, cell) {
|
|
227 var row = cell.parentNode;
|
|
228 var $row = $(row);
|
|
229 // next row is a subrow
|
|
230 var subrows = [];
|
|
231 var $next = $row.next();
|
|
232 while ($next.is("[" + attrs.subRow + "]")) {
|
|
233 subrows.push($next[0]);
|
|
234 $next = $next.next();
|
|
235 }
|
|
236
|
|
237 var tbody = row.parentNode;
|
|
238
|
|
239 // current row is a subrow
|
|
240 if ($row.is("[" + attrs.subRow + "]")) {
|
|
241 } else if (tbody === belongingToTbody) {
|
|
242 cells.push({
|
|
243 element: cell,
|
|
244 cell: getSortValue(cell),
|
|
245 row: row,
|
|
246 subrows: subrows.length ? subrows : null,
|
|
247 ignored: $row.is("[" + attrs.ignoreRow + "]")
|
|
248 });
|
|
249 }
|
|
250 });
|
|
251 return cells;
|
|
252 }
|
|
253
|
|
254 function getSortFxn(ascending, forceNumeric) {
|
|
255 var fn,
|
|
256 regex = /[^\-\+\d\.]/g;
|
|
257 if (ascending) {
|
|
258 fn = function(a, b) {
|
|
259 if (a.ignored || b.ignored) {
|
|
260 return 0;
|
|
261 }
|
|
262 if (forceNumeric) {
|
|
263 return (
|
|
264 parseFloat(a.cell.replace(regex, "")) - parseFloat(b.cell.replace(regex, ""))
|
|
265 );
|
|
266 } else {
|
|
267 return a.cell.toLowerCase() > b.cell.toLowerCase() ? 1 : -1;
|
|
268 }
|
|
269 };
|
|
270 } else {
|
|
271 fn = function(a, b) {
|
|
272 if (a.ignored || b.ignored) {
|
|
273 return 0;
|
|
274 }
|
|
275 if (forceNumeric) {
|
|
276 return (
|
|
277 parseFloat(b.cell.replace(regex, "")) - parseFloat(a.cell.replace(regex, ""))
|
|
278 );
|
|
279 } else {
|
|
280 return a.cell.toLowerCase() < b.cell.toLowerCase() ? 1 : -1;
|
|
281 }
|
|
282 };
|
|
283 }
|
|
284 return fn;
|
|
285 }
|
|
286
|
|
287 function convertToRows(sorted) {
|
|
288 var newRows = [],
|
|
289 i,
|
|
290 l;
|
|
291 for (i = 0, l = sorted.length; i < l; i++) {
|
|
292 newRows.push(sorted[i].row);
|
|
293 if (sorted[i].subrows) {
|
|
294 newRows.push(sorted[i].subrows);
|
|
295 }
|
|
296 }
|
|
297 return newRows;
|
|
298 }
|
|
299
|
|
300 var fn;
|
|
301 var sorted;
|
|
302 var cells = convertCells(col.cells, tbody);
|
|
303
|
|
304 var customFn = $(col).data("tablesaw-sort");
|
|
305
|
|
306 fn =
|
|
307 (customFn && typeof customFn === "function" ? customFn(ascending) : false) ||
|
|
308 getSortFxn(
|
|
309 ascending,
|
|
310 $(col).is("[" + attrs.numericCol + "]") &&
|
|
311 !$(col).is("[" + attrs.numericCol + '="false"]')
|
|
312 );
|
|
313
|
|
314 sorted = cells.sort(fn);
|
|
315
|
|
316 rows = convertToRows(sorted);
|
|
317
|
|
318 return rows;
|
|
319 },
|
|
320 makeColDefault: function(col, a) {
|
|
321 var c = $(col);
|
|
322 c.attr(attrs.defaultCol, "true");
|
|
323 if (a) {
|
|
324 c.removeClass(classes.descend);
|
|
325 c.addClass(classes.ascend);
|
|
326 } else {
|
|
327 c.removeClass(classes.ascend);
|
|
328 c.addClass(classes.descend);
|
|
329 }
|
|
330 },
|
|
331 sortBy: function(col, ascending) {
|
|
332 var el = $(this);
|
|
333 var colNum;
|
|
334 var tbl = el.data("tablesaw");
|
|
335 tbl.$tbody.each(function() {
|
|
336 var tbody = this;
|
|
337 var $tbody = $(this);
|
|
338 var rows = tbl.getBodyRows(tbody);
|
|
339 var sortedRows;
|
|
340 var map = tbl.headerMapping[0];
|
|
341 var j, k;
|
|
342
|
|
343 // find the column number that we’re sorting
|
|
344 for (j = 0, k = map.length; j < k; j++) {
|
|
345 if (map[j] === col) {
|
|
346 colNum = j;
|
|
347 break;
|
|
348 }
|
|
349 }
|
|
350
|
|
351 sortedRows = el[pluginName]("sortRows", rows, colNum, ascending, col, tbody);
|
|
352
|
|
353 // replace Table rows
|
|
354 for (j = 0, k = sortedRows.length; j < k; j++) {
|
|
355 $tbody.append(sortedRows[j]);
|
|
356 }
|
|
357 });
|
|
358
|
|
359 el[pluginName]("makeColDefault", col, ascending);
|
|
360
|
|
361 el.trigger("tablesaw-sorted");
|
|
362 }
|
|
363 };
|
|
364
|
|
365 // Collection method.
|
|
366 $.fn[pluginName] = function(arrg) {
|
|
367 var args = Array.prototype.slice.call(arguments, 1),
|
|
368 returnVal;
|
|
369
|
|
370 // if it's a method
|
|
371 if (arrg && typeof arrg === "string") {
|
|
372 returnVal = $.fn[pluginName].prototype[arrg].apply(this[0], args);
|
|
373 return typeof returnVal !== "undefined" ? returnVal : $(this);
|
|
374 }
|
|
375 // check init
|
|
376 if (!$(this).data(pluginName + "-active")) {
|
|
377 $(this).data(pluginName + "-active", true);
|
|
378 $.fn[pluginName].prototype._create.call(this, arrg);
|
|
379 }
|
|
380 return $(this);
|
|
381 };
|
|
382 // add methods
|
|
383 $.extend($.fn[pluginName].prototype, methods);
|
|
384
|
|
385 $(document).on(Tablesaw.events.create, function(e, Tablesaw) {
|
|
386 if (Tablesaw.$table.is(initSelector)) {
|
|
387 Tablesaw.$table[pluginName]();
|
|
388 }
|
|
389 });
|
|
390
|
|
391 // TODO OOP this and add to Tablesaw object
|
|
392 })();
|