13 function Tablesort(el, options) {
14 if (!(this instanceof Tablesort)) return new Tablesort(el, options);
16 if (!el || el.tagName !== 'TABLE') {
17 throw new Error('Element must be a table');
19 this.init(el, options || {});
24 var createEvent = function(name) {
27 if (!window.CustomEvent || typeof window.CustomEvent !== 'function') {
28 evt = document.createEvent('CustomEvent');
29 evt.initCustomEvent(name, false, false, undefined);
31 evt = new CustomEvent(name);
37 var getInnerText = function(el) {
38 return el.getAttribute('data-sort') || el.textContent || el.innerText || '';
41 // Default sort method if no better sort method is found
42 var caseInsensitiveSort = function(a, b) {
46 if (a === b) return 0;
52 // Stable sort function
53 // If two elements are equal under the original sort function,
54 // then there relative order is reversed
55 var stabilize = function(sort, antiStabilize) {
56 return function(a, b) {
57 var unstableResult = sort(a.td, b.td);
59 if (unstableResult === 0) {
60 if (antiStabilize) return b.index - a.index;
61 return a.index - b.index;
64 return unstableResult;
68 Tablesort.extend = function(name, pattern, sort) {
69 if (typeof pattern !== 'function' || typeof sort !== 'function') {
70 throw new Error('Pattern and sort must be a function');
80 Tablesort.prototype = {
82 init: function(el, options) {
91 that.options = options;
93 if (el.rows && el.rows.length > 0) {
94 if (el.tHead && el.tHead.rows.length > 0) {
95 for (i = 0; i < el.tHead.rows.length; i++) {
96 if (el.tHead.rows[i].getAttribute('data-sort-method') === 'thead') {
97 firstRow = el.tHead.rows[i];
102 firstRow = el.tHead.rows[el.tHead.rows.length - 1];
106 firstRow = el.rows[0];
110 if (!firstRow) return;
112 var onClick = function() {
113 if (that.current && that.current !== this) {
114 that.current.removeAttribute('aria-sort');
118 that.sortTable(this);
121 // Assume first row is the header and attach a click handler to each.
122 for (i = 0; i < firstRow.cells.length; i++) {
123 cell = firstRow.cells[i];
124 cell.setAttribute('role','columnheader');
125 if (cell.getAttribute('data-sort-method') !== 'none') {
127 cell.addEventListener('click', onClick, false);
129 if (cell.getAttribute('data-sort-default') !== null) {
136 that.current = defaultSort;
137 that.sortTable(defaultSort);
141 sortTable: function(header, update) {
143 column = header.cellIndex,
144 sortFunction = caseInsensitiveSort,
147 i = that.thead ? 0 : 1,
148 sortMethod = header.getAttribute('data-sort-method'),
149 sortOrder = header.getAttribute('aria-sort');
151 that.table.dispatchEvent(createEvent('beforeSort'));
153 // If updating an existing sort, direction should remain unchanged.
155 if (sortOrder === 'ascending') {
156 sortOrder = 'descending';
157 } else if (sortOrder === 'descending') {
158 sortOrder = 'ascending';
160 sortOrder = that.options.descending ? 'ascending' : 'descending';
163 header.setAttribute('aria-sort', sortOrder);
166 if (that.table.rows.length < 2) return;
168 // If we force a sort method, it is not necessary to check rows
170 while (items.length < 3 && i < that.table.tBodies[0].rows.length) {
171 item = getInnerText(that.table.tBodies[0].rows[i].cells[column]);
174 if (item.length > 0) {
184 for (i = 0; i < sortOptions.length; i++) {
185 item = sortOptions[i];
188 if (item.name === sortMethod) {
189 sortFunction = item.sort;
192 } else if (items.every(item.pattern)) {
193 sortFunction = item.sort;
200 for (i = 0; i < that.table.tBodies.length; i++) {
207 if (that.table.tBodies[i].rows.length < 2) continue;
209 for (j = 0; j < that.table.tBodies[i].rows.length; j++) {
210 item = that.table.tBodies[i].rows[j];
211 if (item.getAttribute('data-sort-method') === 'none') {
212 // keep no-sorts in separate list to be able to insert
213 // them back at their original position later
214 noSorts[totalRows] = item;
216 // Save the index for stable sorting
219 td: getInnerText(item.cells[that.col]),
225 // Before we append should we reverse the new array or not?
226 // If we reverse, the sort needs to be `anti-stable` so that
227 // the double negatives cancel out
228 if (sortOrder === 'descending') {
229 newRows.sort(stabilize(sortFunction, true));
232 newRows.sort(stabilize(sortFunction, false));
235 // append rows that already exist rather than creating new ones
236 for (j = 0; j < totalRows; j++) {
238 // We have a no-sort row for this position, insert it here.
242 item = newRows[j - noSortsSoFar].tr;
245 // appendChild(x) moves x if already present somewhere else in the DOM
246 that.table.tBodies[i].appendChild(item);
250 that.table.dispatchEvent(createEvent('afterSort'));
253 refresh: function() {
254 if (this.current !== undefined) {
255 this.sortTable(this.current, true);
260 if (typeof module !== 'undefined' && module.exports) {
261 module.exports = Tablesort;
263 window.Tablesort = Tablesort;