Black_Dashboard_Pro/assets/js/plugins/jquery.tablesorter.js

3023 lines
122 KiB
JavaScript

(function(factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory(require('jquery'));
} else {
factory(jQuery);
}
}(function(jQuery) {
/*! TableSorter (FORK) v2.31.1 */
/*
* Client-side table sorting with ease!
* @requires jQuery v1.2.6+
*
* Copyright (c) 2007 Christian Bach
* fork maintained by Rob Garrison
*
* Examples and original docs at: http://tablesorter.com
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* @type jQuery
* @name tablesorter (FORK)
* @cat Plugins/Tablesorter
* @author Christian Bach - christian.bach@polyester.se
* @contributor Rob Garrison - https://github.com/Mottie/tablesorter
* @docs (fork) - https://mottie.github.io/tablesorter/docs/
*/
/*jshint browser:true, jquery:true, unused:false, expr: true */
;
(function($) {
'use strict';
var ts = $.tablesorter = {
version: '2.31.1',
parsers: [],
widgets: [],
defaults: {
// *** appearance
theme: 'default', // adds tablesorter-{theme} to the table for styling
widthFixed: false, // adds colgroup to fix widths of columns
showProcessing: false, // show an indeterminate timer icon in the header when the table is sorted or filtered.
headerTemplate: '{content}', // header layout template (HTML ok); {content} = innerHTML, {icon} = <i/> // class from cssIcon
onRenderTemplate: null, // function( index, template ) { return template; }, // template is a string
onRenderHeader: null, // function( index ) {}, // nothing to return
// *** functionality
cancelSelection: true, // prevent text selection in the header
tabIndex: true, // add tabindex to header for keyboard accessibility
dateFormat: 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd'
sortMultiSortKey: 'shiftKey', // key used to select additional columns
sortResetKey: 'ctrlKey', // key used to remove sorting on a column
usNumberFormat: true, // false for German '1.234.567,89' or French '1 234 567,89'
delayInit: false, // if false, the parsed table contents will not update until the first sort
serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used.
resort: true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed
// *** sort options
headers: {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc.
ignoreCase: true, // ignore case while sorting
sortForce: null, // column(s) first sorted; always applied
sortList: [], // Initial sort order; applied initially; updated when manually sorted
sortAppend: null, // column(s) sorted last; always applied
sortStable: false, // when sorting two rows with exactly the same content, the original sort order is maintained
sortInitialOrder: 'asc', // sort direction on first click
sortLocaleCompare: false, // replace equivalent character (accented characters)
sortReset: false, // third click on the header will reset column to default - unsorted
sortRestart: false, // restart sort to 'sortInitialOrder' when clicking on previously unsorted columns
emptyTo: 'bottom', // sort empty cell to bottom, top, none, zero, emptyMax, emptyMin
stringTo: 'max', // sort strings in numerical column as max, min, top, bottom, zero
duplicateSpan: true, // colspan cells in the tbody will have duplicated content in the cache for each spanned column
textExtraction: 'basic', // text extraction method/function - function( node, table, cellIndex ) {}
textAttribute: 'data-text', // data-attribute that contains alternate cell text (used in default textExtraction function)
textSorter: null, // choose overall or specific column sorter function( a, b, direction, table, columnIndex ) [alt: ts.sortText]
numberSorter: null, // choose overall numeric sorter function( a, b, direction, maxColumnValue )
// *** widget options
initWidgets: true, // apply widgets on tablesorter initialization
widgetClass: 'widget-{name}', // table class name template to match to include a widget
widgets: [], // method to add widgets, e.g. widgets: ['zebra']
widgetOptions: {
zebra: ['even', 'odd'] // zebra widget alternating row class names
},
// *** callbacks
initialized: null, // function( table ) {},
// *** extra css class names
tableClass: '',
cssAsc: '',
cssDesc: '',
cssNone: '',
cssHeader: '',
cssHeaderRow: '',
cssProcessing: '', // processing icon applied to header during sort/filter
cssChildRow: 'tablesorter-childRow', // class name indiciating that a row is to be attached to its parent
cssInfoBlock: 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!)
cssNoSort: 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort
cssIgnoreRow: 'tablesorter-ignoreRow', // header row to ignore; cells within this row will not be added to c.$headers
cssIcon: 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate
cssIconNone: '', // class name added to the icon when there is no column sort
cssIconAsc: '', // class name added to the icon when the column has an ascending sort
cssIconDesc: '', // class name added to the icon when the column has a descending sort
cssIconDisabled: '', // class name added to the icon when the column has a disabled sort
// *** events
pointerClick: 'click',
pointerDown: 'mousedown',
pointerUp: 'mouseup',
// *** selectors
selectorHeaders: '> thead th, > thead td',
selectorSort: 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort
selectorRemove: '.remove-me',
// *** advanced
debug: false,
// *** Internal variables
headerList: [],
empties: {},
strings: {},
parsers: [],
// *** parser options for validator; values must be falsy!
globalize: 0,
imgAttr: 0
// removed: widgetZebra: { css: ['even', 'odd'] }
},
// internal css classes - these will ALWAYS be added to
// the table and MUST only contain one class name - fixes #381
css: {
table: 'tablesorter',
cssHasChild: 'tablesorter-hasChildRow',
childRow: 'tablesorter-childRow',
colgroup: 'tablesorter-colgroup',
header: 'tablesorter-header',
headerRow: 'tablesorter-headerRow',
headerIn: 'tablesorter-header-inner',
icon: 'tablesorter-icon',
processing: 'tablesorter-processing',
sortAsc: 'tablesorter-headerAsc',
sortDesc: 'tablesorter-headerDesc',
sortNone: 'tablesorter-headerUnSorted'
},
// labels applied to sortable headers for accessibility (aria) support
language: {
sortAsc: 'Ascending sort applied, ',
sortDesc: 'Descending sort applied, ',
sortNone: 'No sort applied, ',
sortDisabled: 'sorting is disabled',
nextAsc: 'activate to apply an ascending sort',
nextDesc: 'activate to apply a descending sort',
nextNone: 'activate to remove the sort'
},
regex: {
templateContent: /\{content\}/g,
templateIcon: /\{icon\}/g,
templateName: /\{name\}/i,
spaces: /\s+/g,
nonWord: /\W/g,
formElements: /(input|select|button|textarea)/i,
// *** sort functions ***
// regex used in natural sort
// chunk/tokenize numbers & letters
chunk: /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi,
// replace chunks @ ends
chunks: /(^\\0|\\0$)/,
hex: /^0x[0-9a-f]+$/i,
// *** formatFloat ***
comma: /,/g,
digitNonUS: /[\s|\.]/g,
digitNegativeTest: /^\s*\([.\d]+\)/,
digitNegativeReplace: /^\s*\(([.\d]+)\)/,
// *** isDigit ***
digitTest: /^[\-+(]?\d+[)]?$/,
digitReplace: /[,.'"\s]/g
},
// digit sort, text location
string: {
max: 1,
min: -1,
emptymin: 1,
emptymax: -1,
zero: 0,
none: 0,
'null': 0,
top: true,
bottom: false
},
keyCodes: {
enter: 13
},
// placeholder date parser data (globalize)
dates: {},
// These methods can be applied on table.config instance
instanceMethods: {},
/*
▄█████ ██████ ██████ ██ ██ █████▄
▀█▄ ██▄▄ ██ ██ ██ ██▄▄██
▀█▄ ██▀▀ ██ ██ ██ ██▀▀▀
█████▀ ██████ ██ ▀████▀ ██
*/
setup: function(table, c) {
// if no thead or tbody, or tablesorter is already present, quit
if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) {
if (ts.debug(c, 'core')) {
if (table.hasInitialized) {
console.warn('Stopping initialization. Tablesorter has already been initialized');
} else {
console.error('Stopping initialization! No table, thead or tbody', table);
}
}
return;
}
var tmp = '',
$table = $(table),
meta = $.metadata;
// initialization flag
table.hasInitialized = false;
// table is being processed flag
table.isProcessing = true;
// make sure to store the config object
table.config = c;
// save the settings where they read
$.data(table, 'tablesorter', c);
if (ts.debug(c, 'core')) {
console[console.group ? 'group' : 'log']('Initializing tablesorter v' + ts.version);
$.data(table, 'startoveralltimer', new Date());
}
// removing this in version 3 (only supports jQuery 1.7+)
c.supportsDataObject = (function(version) {
version[0] = parseInt(version[0], 10);
return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4);
})($.fn.jquery.split('.'));
// ensure case insensitivity
c.emptyTo = c.emptyTo.toLowerCase();
c.stringTo = c.stringTo.toLowerCase();
c.last = {
sortList: [],
clickedIndex: -1
};
// add table theme class only if there isn't already one there
if (!/tablesorter\-/.test($table.attr('class'))) {
tmp = (c.theme !== '' ? ' tablesorter-' + c.theme : '');
}
// give the table a unique id, which will be used in namespace binding
if (!c.namespace) {
c.namespace = '.tablesorter' + Math.random().toString(16).slice(2);
} else {
// make sure namespace starts with a period & doesn't have weird characters
c.namespace = '.' + c.namespace.replace(ts.regex.nonWord, '');
}
c.table = table;
c.$table = $table
// add namespace to table to allow bindings on extra elements to target
// the parent table (e.g. parser-input-select)
.addClass(ts.css.table + ' ' + c.tableClass + tmp + ' ' + c.namespace.slice(1))
.attr('role', 'grid');
c.$headers = $table.find(c.selectorHeaders);
c.$table.children().children('tr').attr('role', 'row');
c.$tbodies = $table.children('tbody:not(.' + c.cssInfoBlock + ')').attr({
'aria-live': 'polite',
'aria-relevant': 'all'
});
if (c.$table.children('caption').length) {
tmp = c.$table.children('caption')[0];
if (!tmp.id) {
tmp.id = c.namespace.slice(1) + 'caption';
}
c.$table.attr('aria-labelledby', tmp.id);
}
c.widgetInit = {}; // keep a list of initialized widgets
// change textExtraction via data-attribute
c.textExtraction = c.$table.attr('data-text-extraction') || c.textExtraction || 'basic';
// build headers
ts.buildHeaders(c);
// fixate columns if the users supplies the fixedWidth option
// do this after theme has been applied
ts.fixColumnWidth(table);
// add widgets from class name
ts.addWidgetFromClass(table);
// add widget options before parsing (e.g. grouping widget has parser settings)
ts.applyWidgetOptions(table);
// try to auto detect column type, and store in tables config
ts.setupParsers(c);
// start total row count at zero
c.totalRows = 0;
// only validate options while debugging. See #1528
if (c.debug) {
ts.validateOptions(c);
}
// build the cache for the tbody cells
// delayInit will delay building the cache until the user starts a sort
if (!c.delayInit) {
ts.buildCache(c);
}
// bind all header events and methods
ts.bindEvents(table, c.$headers, true);
ts.bindMethods(c);
// get sort list from jQuery data or metadata
// in jQuery < 1.4, an error occurs when calling $table.data()
if (c.supportsDataObject && typeof $table.data().sortlist !== 'undefined') {
c.sortList = $table.data().sortlist;
} else if (meta && ($table.metadata() && $table.metadata().sortlist)) {
c.sortList = $table.metadata().sortlist;
}
// apply widget init code
ts.applyWidget(table, true);
// if user has supplied a sort list to constructor
if (c.sortList.length > 0) {
// save sortList before any sortAppend is added
c.last.sortList = c.sortList;
ts.sortOn(c, c.sortList, {}, !c.initWidgets);
} else {
ts.setHeadersCss(c);
if (c.initWidgets) {
// apply widget format
ts.applyWidget(table, false);
}
}
// show processesing icon
if (c.showProcessing) {
$table
.unbind('sortBegin' + c.namespace + ' sortEnd' + c.namespace)
.bind('sortBegin' + c.namespace + ' sortEnd' + c.namespace, function(e) {
clearTimeout(c.timerProcessing);
ts.isProcessing(table);
if (e.type === 'sortBegin') {
c.timerProcessing = setTimeout(function() {
ts.isProcessing(table, true);
}, 500);
}
});
}
// initialized
table.hasInitialized = true;
table.isProcessing = false;
if (ts.debug(c, 'core')) {
console.log('Overall initialization time:' + ts.benchmark($.data(table, 'startoveralltimer')));
if (ts.debug(c, 'core') && console.groupEnd) {
console.groupEnd();
}
}
$table.triggerHandler('tablesorter-initialized', table);
if (typeof c.initialized === 'function') {
c.initialized(table);
}
},
bindMethods: function(c) {
var $table = c.$table,
namespace = c.namespace,
events = ('sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' +
'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' +
'mouseleave ').split(' ')
.join(namespace + ' ');
// apply easy methods that trigger bound events
$table
.unbind(events.replace(ts.regex.spaces, ' '))
.bind('sortReset' + namespace, function(e, callback) {
e.stopPropagation();
// using this.config to ensure functions are getting a non-cached version of the config
ts.sortReset(this.config, function(table) {
if (table.isApplyingWidgets) {
// multiple triggers in a row... filterReset, then sortReset - see #1361
// wait to update widgets
setTimeout(function() {
ts.applyWidget(table, '', callback);
}, 100);
} else {
ts.applyWidget(table, '', callback);
}
});
})
.bind('updateAll' + namespace, function(e, resort, callback) {
e.stopPropagation();
ts.updateAll(this.config, resort, callback);
})
.bind('update' + namespace + ' updateRows' + namespace, function(e, resort, callback) {
e.stopPropagation();
ts.update(this.config, resort, callback);
})
.bind('updateHeaders' + namespace, function(e, callback) {
e.stopPropagation();
ts.updateHeaders(this.config, callback);
})
.bind('updateCell' + namespace, function(e, cell, resort, callback) {
e.stopPropagation();
ts.updateCell(this.config, cell, resort, callback);
})
.bind('addRows' + namespace, function(e, $row, resort, callback) {
e.stopPropagation();
ts.addRows(this.config, $row, resort, callback);
})
.bind('updateComplete' + namespace, function() {
this.isUpdating = false;
})
.bind('sorton' + namespace, function(e, list, callback, init) {
e.stopPropagation();
ts.sortOn(this.config, list, callback, init);
})
.bind('appendCache' + namespace, function(e, callback, init) {
e.stopPropagation();
ts.appendCache(this.config, init);
if ($.isFunction(callback)) {
callback(this);
}
})
// $tbodies variable is used by the tbody sorting widget
.bind('updateCache' + namespace, function(e, callback, $tbodies) {
e.stopPropagation();
ts.updateCache(this.config, callback, $tbodies);
})
.bind('applyWidgetId' + namespace, function(e, id) {
e.stopPropagation();
ts.applyWidgetId(this, id);
})
.bind('applyWidgets' + namespace, function(e, callback) {
e.stopPropagation();
// apply widgets (false = not initializing)
ts.applyWidget(this, false, callback);
})
.bind('refreshWidgets' + namespace, function(e, all, dontapply) {
e.stopPropagation();
ts.refreshWidgets(this, all, dontapply);
})
.bind('removeWidget' + namespace, function(e, name, refreshing) {
e.stopPropagation();
ts.removeWidget(this, name, refreshing);
})
.bind('destroy' + namespace, function(e, removeClasses, callback) {
e.stopPropagation();
ts.destroy(this, removeClasses, callback);
})
.bind('resetToLoadState' + namespace, function(e) {
e.stopPropagation();
// remove all widgets
ts.removeWidget(this, true, false);
var tmp = $.extend(true, {}, c.originalSettings);
// restore original settings; this clears out current settings, but does not clear
// values saved to storage.
c = $.extend(true, {}, ts.defaults, tmp);
c.originalSettings = tmp;
this.hasInitialized = false;
// setup the entire table again
ts.setup(this, c);
});
},
bindEvents: function(table, $headers, core) {
table = $(table)[0];
var tmp,
c = table.config,
namespace = c.namespace,
downTarget = null;
if (core !== true) {
$headers.addClass(namespace.slice(1) + '_extra_headers');
tmp = ts.getClosest($headers, 'table');
if (tmp.length && tmp[0].nodeName === 'TABLE' && tmp[0] !== table) {
$(tmp[0]).addClass(namespace.slice(1) + '_extra_table');
}
}
tmp = (c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ')
.replace(ts.regex.spaces, ' ')
.split(' ')
.join(namespace + ' ');
// apply event handling to headers and/or additional headers (stickyheaders, scroller, etc)
$headers
// http://stackoverflow.com/questions/5312849/jquery-find-self;
.find(c.selectorSort)
.add($headers.filter(c.selectorSort))
.unbind(tmp)
.bind(tmp, function(e, external) {
var $cell, cell, temp,
$target = $(e.target),
// wrap event type in spaces, so the match doesn't trigger on inner words
type = ' ' + e.type + ' ';
// only recognize left clicks
if (((e.which || e.button) !== 1 && !type.match(' ' + c.pointerClick + ' | sort | keyup ')) ||
// allow pressing enter
(type === ' keyup ' && e.which !== ts.keyCodes.enter) ||
// allow triggering a click event (e.which is undefined) & ignore physical clicks
(type.match(' ' + c.pointerClick + ' ') && typeof e.which !== 'undefined')) {
return;
}
// ignore mouseup if mousedown wasn't on the same target
if (type.match(' ' + c.pointerUp + ' ') && downTarget !== e.target && external !== true) {
return;
}
// set target on mousedown
if (type.match(' ' + c.pointerDown + ' ')) {
downTarget = e.target;
// preventDefault needed or jQuery v1.3.2 and older throws an
// "Uncaught TypeError: handler.apply is not a function" error
temp = $target.jquery.split('.');
if (temp[0] === '1' && temp[1] < 4) {
e.preventDefault();
}
return;
}
downTarget = null;
$cell = ts.getClosest($(this), '.' + ts.css.header);
// prevent sort being triggered on form elements
if (ts.regex.formElements.test(e.target.nodeName) ||
// nosort class name, or elements within a nosort container
$target.hasClass(c.cssNoSort) || $target.parents('.' + c.cssNoSort).length > 0 ||
// disabled cell directly clicked
$cell.hasClass('sorter-false') ||
// elements within a button
$target.parents('button').length > 0) {
return !c.cancelSelection;
}
if (c.delayInit && ts.isEmptyObject(c.cache)) {
ts.buildCache(c);
}
// use column index from data-attribute or index of current row; fixes #1116
c.last.clickedIndex = $cell.attr('data-column') || $cell.index();
cell = c.$headerIndexed[c.last.clickedIndex][0];
if (cell && !cell.sortDisabled) {
ts.initSort(c, cell, e);
}
});
if (c.cancelSelection) {
// cancel selection
$headers
.attr('unselectable', 'on')
.bind('selectstart', false)
.css({
'user-select': 'none',
'MozUserSelect': 'none' // not needed for jQuery 1.8+
});
}
},
buildHeaders: function(c) {
var $temp, icon, timer, indx;
c.headerList = [];
c.headerContent = [];
c.sortVars = [];
if (ts.debug(c, 'core')) {
timer = new Date();
}
// children tr in tfoot - see issue #196 & #547
// don't pass table.config to computeColumnIndex here - widgets (math) pass it to "quickly" index tbody cells
c.columns = ts.computeColumnIndex(c.$table.children('thead, tfoot').children('tr'));
// add icon if cssIcon option exists
icon = c.cssIcon ?
'<i class="' + (c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon) + '"></i>' :
'';
// redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683
c.$headers = $($.map(c.$table.find(c.selectorHeaders), function(elem, index) {
var configHeaders, header, column, template, tmp,
$elem = $(elem);
// ignore cell (don't add it to c.$headers) if row has ignoreRow class
if (ts.getClosest($elem, 'tr').hasClass(c.cssIgnoreRow)) {
return;
}
// transfer data-column to element if not th/td - #1459
if (!/(th|td)/i.test(elem.nodeName)) {
tmp = ts.getClosest($elem, 'th, td');
$elem.attr('data-column', tmp.attr('data-column'));
}
// make sure to get header cell & not column indexed cell
configHeaders = ts.getColumnData(c.table, c.headers, index, true);
// save original header content
c.headerContent[index] = $elem.html();
// if headerTemplate is empty, don't reformat the header cell
if (c.headerTemplate !== '' && !$elem.find('.' + ts.css.headerIn).length) {
// set up header template
template = c.headerTemplate
.replace(ts.regex.templateContent, $elem.html())
.replace(ts.regex.templateIcon, $elem.find('.' + ts.css.icon).length ? '' : icon);
if (c.onRenderTemplate) {
header = c.onRenderTemplate.apply($elem, [index, template]);
// only change t if something is returned
if (header && typeof header === 'string') {
template = header;
}
}
$elem.html('<div class="' + ts.css.headerIn + '">' + template + '</div>'); // faster than wrapInner
}
if (c.onRenderHeader) {
c.onRenderHeader.apply($elem, [index, c, c.$table]);
}
column = parseInt($elem.attr('data-column'), 10);
elem.column = column;
tmp = ts.getOrder(ts.getData($elem, configHeaders, 'sortInitialOrder') || c.sortInitialOrder);
// this may get updated numerous times if there are multiple rows
c.sortVars[column] = {
count: -1, // set to -1 because clicking on the header automatically adds one
order: tmp ?
(c.sortReset ? [1, 0, 2] : [1, 0]) : // desc, asc, unsorted
(c.sortReset ? [0, 1, 2] : [0, 1]), // asc, desc, unsorted
lockedOrder: false,
sortedBy: ''
};
tmp = ts.getData($elem, configHeaders, 'lockedOrder') || false;
if (typeof tmp !== 'undefined' && tmp !== false) {
c.sortVars[column].lockedOrder = true;
c.sortVars[column].order = ts.getOrder(tmp) ? [1, 1] : [0, 0];
}
// add cell to headerList
c.headerList[index] = elem;
$elem.addClass(ts.css.header + ' ' + c.cssHeader);
// add to parent in case there are multiple rows
ts.getClosest($elem, 'tr')
.addClass(ts.css.headerRow + ' ' + c.cssHeaderRow)
.attr('role', 'row');
// allow keyboard cursor to focus on element
if (c.tabIndex) {
$elem.attr('tabindex', 0);
}
return elem;
}));
// cache headers per column
c.$headerIndexed = [];
for (indx = 0; indx < c.columns; indx++) {
// colspan in header making a column undefined
if (ts.isEmptyObject(c.sortVars[indx])) {
c.sortVars[indx] = {};
}
// Use c.$headers.parent() in case selectorHeaders doesn't point to the th/td
$temp = c.$headers.filter('[data-column="' + indx + '"]');
// target sortable column cells, unless there are none, then use non-sortable cells
// .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6
c.$headerIndexed[indx] = $temp.length ?
$temp.not('.sorter-false').length ?
$temp.not('.sorter-false').filter(':last') :
$temp.filter(':last') :
$();
}
c.$table.find(c.selectorHeaders).attr({
scope: 'col',
role: 'columnheader'
});
// enable/disable sorting
ts.updateHeader(c);
if (ts.debug(c, 'core')) {
console.log('Built headers:' + ts.benchmark(timer));
console.log(c.$headers);
}
},
// Use it to add a set of methods to table.config which will be available for all tables.
// This should be done before table initialization
addInstanceMethods: function(methods) {
$.extend(ts.instanceMethods, methods);
},
/*
█████▄ ▄████▄ █████▄ ▄█████ ██████ █████▄ ▄█████
██▄▄██ ██▄▄██ ██▄▄██ ▀█▄ ██▄▄ ██▄▄██ ▀█▄
██▀▀▀ ██▀▀██ ██▀██ ▀█▄ ██▀▀ ██▀██ ▀█▄
██ ██ ██ ██ ██ █████▀ ██████ ██ ██ █████▀
*/
setupParsers: function(c, $tbodies) {
var rows, list, span, max, colIndex, indx, header, configHeaders,
noParser, parser, extractor, time, tbody, len,
table = c.table,
tbodyIndex = 0,
debug = ts.debug(c, 'core'),
debugOutput = {};
// update table bodies in case we start with an empty table
c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')');
tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies;
len = tbody.length;
if (len === 0) {
return debug ? console.warn('Warning: *Empty table!* Not building a parser cache') : '';
} else if (debug) {
time = new Date();
console[console.group ? 'group' : 'log']('Detecting parsers for each column');
}
list = {
extractors: [],
parsers: []
};
while (tbodyIndex < len) {
rows = tbody[tbodyIndex].rows;
if (rows.length) {
colIndex = 0;
max = c.columns;
for (indx = 0; indx < max; indx++) {
header = c.$headerIndexed[colIndex];
if (header && header.length) {
// get column indexed table cell; adding true parameter fixes #1362 but
// it would break backwards compatibility...
configHeaders = ts.getColumnData(table, c.headers, colIndex); // , true );
// get column parser/extractor
extractor = ts.getParserById(ts.getData(header, configHeaders, 'extractor'));
parser = ts.getParserById(ts.getData(header, configHeaders, 'sorter'));
noParser = ts.getData(header, configHeaders, 'parser') === 'false';
// empty cells behaviour - keeping emptyToBottom for backwards compatibility
c.empties[colIndex] = (
ts.getData(header, configHeaders, 'empty') ||
c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top')).toLowerCase();
// text strings behaviour in numerical sorts
c.strings[colIndex] = (
ts.getData(header, configHeaders, 'string') ||
c.stringTo ||
'max').toLowerCase();
if (noParser) {
parser = ts.getParserById('no-parser');
}
if (!extractor) {
// For now, maybe detect someday
extractor = false;
}
if (!parser) {
parser = ts.detectParserForColumn(c, rows, -1, colIndex);
}
if (debug) {
debugOutput['(' + colIndex + ') ' + header.text()] = {
parser: parser.id,
extractor: extractor ? extractor.id : 'none',
string: c.strings[colIndex],
empty: c.empties[colIndex]
};
}
list.parsers[colIndex] = parser;
list.extractors[colIndex] = extractor;
span = header[0].colSpan - 1;
if (span > 0) {
colIndex += span;
max += span;
while (span + 1 > 0) {
// set colspan columns to use the same parsers & extractors
list.parsers[colIndex - span] = parser;
list.extractors[colIndex - span] = extractor;
span--;
}
}
}
colIndex++;
}
}
tbodyIndex += (list.parsers.length) ? len : 1;
}
if (debug) {
if (!ts.isEmptyObject(debugOutput)) {
console[console.table ? 'table' : 'log'](debugOutput);
} else {
console.warn(' No parsers detected!');
}
console.log('Completed detecting parsers' + ts.benchmark(time));
if (console.groupEnd) {
console.groupEnd();
}
}
c.parsers = list.parsers;
c.extractors = list.extractors;
},
addParser: function(parser) {
var indx,
len = ts.parsers.length,
add = true;
for (indx = 0; indx < len; indx++) {
if (ts.parsers[indx].id.toLowerCase() === parser.id.toLowerCase()) {
add = false;
}
}
if (add) {
ts.parsers[ts.parsers.length] = parser;
}
},
getParserById: function(name) {
/*jshint eqeqeq:false */ // eslint-disable-next-line eqeqeq
if (name == 'false') {
return false;
}
var indx,
len = ts.parsers.length;
for (indx = 0; indx < len; indx++) {
if (ts.parsers[indx].id.toLowerCase() === (name.toString()).toLowerCase()) {
return ts.parsers[indx];
}
}
return false;
},
detectParserForColumn: function(c, rows, rowIndex, cellIndex) {
var cur, $node, row,
indx = ts.parsers.length,
node = false,
nodeValue = '',
debug = ts.debug(c, 'core'),
keepLooking = true;
while (nodeValue === '' && keepLooking) {
rowIndex++;
row = rows[rowIndex];
// stop looking after 50 empty rows
if (row && rowIndex < 50) {
if (row.className.indexOf(ts.cssIgnoreRow) < 0) {
node = rows[rowIndex].cells[cellIndex];
nodeValue = ts.getElementText(c, node, cellIndex);
$node = $(node);
if (debug) {
console.log('Checking if value was empty on row ' + rowIndex + ', column: ' +
cellIndex + ': "' + nodeValue + '"');
}
}
} else {
keepLooking = false;
}
}
while (--indx >= 0) {
cur = ts.parsers[indx];
// ignore the default text parser because it will always be true
if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, c.table, node, $node)) {
return cur;
}
}
// nothing found, return the generic parser (text)
return ts.getParserById('text');
},
getElementText: function(c, node, cellIndex) {
if (!node) {
return '';
}
var tmp,
extract = c.textExtraction || '',
// node could be a jquery object
// http://jsperf.com/jquery-vs-instanceof-jquery/2
$node = node.jquery ? node : $(node);
if (typeof extract === 'string') {
// check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow!
// http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/
if (extract === 'basic' && typeof(tmp = $node.attr(c.textAttribute)) !== 'undefined') {
return $.trim(tmp);
}
return $.trim(node.textContent || $node.text());
} else {
if (typeof extract === 'function') {
return $.trim(extract($node[0], c.table, cellIndex));
} else if (typeof(tmp = ts.getColumnData(c.table, extract, cellIndex)) === 'function') {
return $.trim(tmp($node[0], c.table, cellIndex));
}
}
// fallback
return $.trim($node[0].textContent || $node.text());
},
// centralized function to extract/parse cell contents
getParsedText: function(c, cell, colIndex, txt) {
if (typeof txt === 'undefined') {
txt = ts.getElementText(c, cell, colIndex);
}
// if no parser, make sure to return the txt
var val = '' + txt,
parser = c.parsers[colIndex],
extractor = c.extractors[colIndex];
if (parser) {
// do extract before parsing, if there is one
if (extractor && typeof extractor.format === 'function') {
txt = extractor.format(txt, c.table, cell, colIndex);
}
// allow parsing if the string is empty, previously parsing would change it to zero,
// in case the parser needs to extract data from the table cell attributes
val = parser.id === 'no-parser' ? '' :
// make sure txt is a string (extractor may have converted it)
parser.format('' + txt, c.table, cell, colIndex);
if (c.ignoreCase && typeof val === 'string') {
val = val.toLowerCase();
}
}
return val;
},
/*
▄████▄ ▄████▄ ▄████▄ ██ ██ ██████
██ ▀▀ ██▄▄██ ██ ▀▀ ██▄▄██ ██▄▄
██ ▄▄ ██▀▀██ ██ ▄▄ ██▀▀██ ██▀▀
▀████▀ ██ ██ ▀████▀ ██ ██ ██████
*/
buildCache: function(c, callback, $tbodies) {
var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row,
cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData,
colMax, span, cacheIndex, hasParser, max, len, index,
table = c.table,
parsers = c.parsers,
debug = ts.debug(c, 'core');
// update tbody variable
c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')');
$tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies,
c.cache = {};
c.totalRows = 0;
// if no parsers found, return - it's an empty table.
if (!parsers) {
return debug ? console.warn('Warning: *Empty table!* Not building a cache') : '';
}
if (debug) {
cacheTime = new Date();
}
// processing icon
if (c.showProcessing) {
ts.isProcessing(table, true);
}
for (tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++) {
colMax = []; // column max value per tbody
cache = c.cache[tbodyIndex] = {
normalized: [] // array of normalized row data; last entry contains 'rowData' above
// colMax: # // added at the end
};
totalRows = ($tbody[tbodyIndex] && $tbody[tbodyIndex].rows.length) || 0;
for (rowIndex = 0; rowIndex < totalRows; ++rowIndex) {
rowData = {
// order: original row order #
// $row : jQuery Object[]
child: [], // child row text (filter widget)
raw: [] // original row text
};
/** Add the table data to main data array */
$row = $($tbody[tbodyIndex].rows[rowIndex]);
cols = [];
// ignore "remove-me" rows
if ($row.hasClass(c.selectorRemove.slice(1))) {
continue;
}
// if this is a child row, add it to the last row's children and continue to the next row
// ignore child row class, if it is the first row
if ($row.hasClass(c.cssChildRow) && rowIndex !== 0) {
len = cache.normalized.length - 1;
prevRowData = cache.normalized[len][c.columns];
prevRowData.$row = prevRowData.$row.add($row);
// add 'hasChild' class name to parent row
if (!$row.prev().hasClass(c.cssChildRow)) {
$row.prev().addClass(ts.css.cssHasChild);
}
// save child row content (un-parsed!)
$cells = $row.children('th, td');
len = prevRowData.child.length;
prevRowData.child[len] = [];
// child row content does not account for colspans/rowspans; so indexing may be off
cacheIndex = 0;
max = c.columns;
for (colIndex = 0; colIndex < max; colIndex++) {
cell = $cells[colIndex];
if (cell) {
prevRowData.child[len][colIndex] = ts.getParsedText(c, cell, colIndex);
span = $cells[colIndex].colSpan - 1;
if (span > 0) {
cacheIndex += span;
max += span;
}
}
cacheIndex++;
}
// go to the next for loop
continue;
}
rowData.$row = $row;
rowData.order = rowIndex; // add original row position to rowCache
cacheIndex = 0;
max = c.columns;
for (colIndex = 0; colIndex < max; ++colIndex) {
cell = $row[0].cells[colIndex];
if (cell && cacheIndex < c.columns) {
hasParser = typeof parsers[cacheIndex] !== 'undefined';
if (!hasParser && debug) {
console.warn('No parser found for row: ' + rowIndex + ', column: ' + colIndex +
'; cell containing: "' + $(cell).text() + '"; does it have a header?');
}
val = ts.getElementText(c, cell, cacheIndex);
rowData.raw[cacheIndex] = val; // save original row text
// save raw column text even if there is no parser set
txt = ts.getParsedText(c, cell, cacheIndex, val);
cols[cacheIndex] = txt;
if (hasParser && (parsers[cacheIndex].type || '').toLowerCase() === 'numeric') {
// determine column max value (ignore sign)
colMax[cacheIndex] = Math.max(Math.abs(txt) || 0, colMax[cacheIndex] || 0);
}
// allow colSpan in tbody
span = cell.colSpan - 1;
if (span > 0) {
index = 0;
while (index <= span) {
// duplicate text (or not) to spanned columns
// instead of setting duplicate span to empty string, use textExtraction to try to get a value
// see http://stackoverflow.com/q/36449711/145346
txt = c.duplicateSpan || index === 0 ?
val :
typeof c.textExtraction !== 'string' ?
ts.getElementText(c, cell, cacheIndex + index) || '' :
'';
rowData.raw[cacheIndex + index] = txt;
cols[cacheIndex + index] = txt;
index++;
}
cacheIndex += span;
max += span;
}
}
cacheIndex++;
}
// ensure rowData is always in the same location (after the last column)
cols[c.columns] = rowData;
cache.normalized[cache.normalized.length] = cols;
}
cache.colMax = colMax;
// total up rows, not including child rows
c.totalRows += cache.normalized.length;
}
if (c.showProcessing) {
ts.isProcessing(table); // remove processing icon
}
if (debug) {
len = Math.min(5, c.cache[0].normalized.length);
console[console.group ? 'group' : 'log']('Building cache for ' + c.totalRows +
' rows (showing ' + len + ' rows in log) and ' + c.columns + ' columns' +
ts.benchmark(cacheTime));
val = {};
for (colIndex = 0; colIndex < c.columns; colIndex++) {
for (cacheIndex = 0; cacheIndex < len; cacheIndex++) {
if (!val['row: ' + cacheIndex]) {
val['row: ' + cacheIndex] = {};
}
val['row: ' + cacheIndex][c.$headerIndexed[colIndex].text()] =
c.cache[0].normalized[cacheIndex][colIndex];
}
}
console[console.table ? 'table' : 'log'](val);
if (console.groupEnd) {
console.groupEnd();
}
}
if ($.isFunction(callback)) {
callback(table);
}
},
getColumnText: function(table, column, callback, rowFilter) {
table = $(table)[0];
var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result,
hasCallback = typeof callback === 'function',
allColumns = column === 'all',
data = {
raw: [],
parsed: [],
$cell: []
},
c = table.config;
if (ts.isEmptyObject(c)) {
if (ts.debug(c, 'core')) {
console.warn('No cache found - aborting getColumnText function!');
}
} else {
tbodyLen = c.$tbodies.length;
for (tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++) {
cache = c.cache[tbodyIndex].normalized;
rowLen = cache.length;
for (rowIndex = 0; rowIndex < rowLen; rowIndex++) {
row = cache[rowIndex];
if (rowFilter && !row[c.columns].$row.is(rowFilter)) {
continue;
}
result = true;
parsed = (allColumns) ? row.slice(0, c.columns) : row[column];
row = row[c.columns];
raw = (allColumns) ? row.raw : row.raw[column];
$cell = (allColumns) ? row.$row.children() : row.$row.children().eq(column);
if (hasCallback) {
result = callback({
tbodyIndex: tbodyIndex,
rowIndex: rowIndex,
parsed: parsed,
raw: raw,
$row: row.$row,
$cell: $cell
});
}
if (result !== false) {
data.parsed[data.parsed.length] = parsed;
data.raw[data.raw.length] = raw;
data.$cell[data.$cell.length] = $cell;
}
}
}
// return everything
return data;
}
},
/*
██ ██ █████▄ █████▄ ▄████▄ ██████ ██████
██ ██ ██▄▄██ ██ ██ ██▄▄██ ██ ██▄▄
██ ██ ██▀▀▀ ██ ██ ██▀▀██ ██ ██▀▀
▀████▀ ██ █████▀ ██ ██ ██ ██████
*/
setHeadersCss: function(c) {
var indx, column,
list = c.sortList,
len = list.length,
none = ts.css.sortNone + ' ' + c.cssNone,
css = [ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc],
cssIcon = [c.cssIconAsc, c.cssIconDesc, c.cssIconNone],
aria = ['ascending', 'descending'],
updateColumnSort = function($el, index) {
$el
.removeClass(none)
.addClass(css[index])
.attr('aria-sort', aria[index])
.find('.' + ts.css.icon)
.removeClass(cssIcon[2])
.addClass(cssIcon[index]);
},
// find the footer
$extras = c.$table
.find('tfoot tr')
.children('td, th')
.add($(c.namespace + '_extra_headers'))
.removeClass(css.join(' ')),
// remove all header information
$sorted = c.$headers
.add($('thead ' + c.namespace + '_extra_headers'))
.removeClass(css.join(' '))
.addClass(none)
.attr('aria-sort', 'none')
.find('.' + ts.css.icon)
.removeClass(cssIcon.join(' '))
.end();
// add css none to all sortable headers
$sorted
.not('.sorter-false')
.find('.' + ts.css.icon)
.addClass(cssIcon[2]);
// add disabled css icon class
if (c.cssIconDisabled) {
$sorted
.filter('.sorter-false')
.find('.' + ts.css.icon)
.addClass(c.cssIconDisabled);
}
for (indx = 0; indx < len; indx++) {
// direction = 2 means reset!
if (list[indx][1] !== 2) {
// multicolumn sorting updating - see #1005
// .not(function() {}) needs jQuery 1.4
// filter(function(i, el) {}) <- el is undefined in jQuery v1.2.6
$sorted = c.$headers.filter(function(i) {
// only include headers that are in the sortList (this includes colspans)
var include = true,
$el = c.$headers.eq(i),
col = parseInt($el.attr('data-column'), 10),
end = col + ts.getClosest($el, 'th, td')[0].colSpan;
for (; col < end; col++) {
include = include ? include || ts.isValueInArray(col, c.sortList) > -1 : false;
}
return include;
});
// choose the :last in case there are nested columns
$sorted = $sorted
.not('.sorter-false')
.filter('[data-column="' + list[indx][0] + '"]' + (len === 1 ? ':last' : ''));
if ($sorted.length) {
for (column = 0; column < $sorted.length; column++) {
if (!$sorted[column].sortDisabled) {
updateColumnSort($sorted.eq(column), list[indx][1]);
}
}
}
// add sorted class to footer & extra headers, if they exist
if ($extras.length) {
updateColumnSort($extras.filter('[data-column="' + list[indx][0] + '"]'), list[indx][1]);
}
}
}
// add verbose aria labels
len = c.$headers.length;
for (indx = 0; indx < len; indx++) {
ts.setColumnAriaLabel(c, c.$headers.eq(indx));
}
},
getClosest: function($el, selector) {
// jQuery v1.2.6 doesn't have closest()
if ($.fn.closest) {
return $el.closest(selector);
}
return $el.is(selector) ?
$el :
$el.parents(selector).filter(':first');
},
// nextSort (optional), lets you disable next sort text
setColumnAriaLabel: function(c, $header, nextSort) {
if ($header.length) {
var column = parseInt($header.attr('data-column'), 10),
vars = c.sortVars[column],
tmp = $header.hasClass(ts.css.sortAsc) ?
'sortAsc' :
$header.hasClass(ts.css.sortDesc) ? 'sortDesc' : 'sortNone',
txt = $.trim($header.text()) + ': ' + ts.language[tmp];
if ($header.hasClass('sorter-false') || nextSort === false) {
txt += ts.language.sortDisabled;
} else {
tmp = (vars.count + 1) % vars.order.length;
nextSort = vars.order[tmp];
// if nextSort
txt += ts.language[nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone'];
}
$header.attr('aria-label', txt);
if (vars.sortedBy) {
$header.attr('data-sortedBy', vars.sortedBy);
} else {
$header.removeAttr('data-sortedBy');
}
}
},
updateHeader: function(c) {
var index, isDisabled, $header, col,
table = c.table,
len = c.$headers.length;
for (index = 0; index < len; index++) {
$header = c.$headers.eq(index);
col = ts.getColumnData(table, c.headers, index, true);
// add 'sorter-false' class if 'parser-false' is set
isDisabled = ts.getData($header, col, 'sorter') === 'false' || ts.getData($header, col, 'parser') === 'false';
ts.setColumnSort(c, $header, isDisabled);
}
},
setColumnSort: function(c, $header, isDisabled) {
var id = c.table.id;
$header[0].sortDisabled = isDisabled;
$header[isDisabled ? 'addClass' : 'removeClass']('sorter-false')
.attr('aria-disabled', '' + isDisabled);
// disable tab index on disabled cells
if (c.tabIndex) {
if (isDisabled) {
$header.removeAttr('tabindex');
} else {
$header.attr('tabindex', '0');
}
}
// aria-controls - requires table ID
if (id) {
if (isDisabled) {
$header.removeAttr('aria-controls');
} else {
$header.attr('aria-controls', id);
}
}
},
updateHeaderSortCount: function(c, list) {
var col, dir, group, indx, primary, temp, val, order,
sortList = list || c.sortList,
len = sortList.length;
c.sortList = [];
for (indx = 0; indx < len; indx++) {
val = sortList[indx];
// ensure all sortList values are numeric - fixes #127
col = parseInt(val[0], 10);
// prevents error if sorton array is wrong
if (col < c.columns) {
// set order if not already defined - due to colspan header without associated header cell
// adding this check prevents a javascript error
if (!c.sortVars[col].order) {
if (ts.getOrder(c.sortInitialOrder)) {
order = c.sortReset ? [1, 0, 2] : [1, 0];
} else {
order = c.sortReset ? [0, 1, 2] : [0, 1];
}
c.sortVars[col].order = order;
c.sortVars[col].count = 0;
}
order = c.sortVars[col].order;
dir = ('' + val[1]).match(/^(1|d|s|o|n)/);
dir = dir ? dir[0] : '';
// 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext
switch (dir) {
case '1':
case 'd': // descending
dir = 1;
break;
case 's': // same direction (as primary column)
// if primary sort is set to 's', make it ascending
dir = primary || 0;
break;
case 'o':
temp = order[(primary || 0) % order.length];
// opposite of primary column; but resets if primary resets
dir = temp === 0 ? 1 : temp === 1 ? 0 : 2;
break;
case 'n':
dir = order[(++c.sortVars[col].count) % order.length];
break;
default: // ascending
dir = 0;
break;
}
primary = indx === 0 ? dir : primary;
group = [col, parseInt(dir, 10) || 0];
c.sortList[c.sortList.length] = group;
dir = $.inArray(group[1], order); // fixes issue #167
c.sortVars[col].count = dir >= 0 ? dir : group[1] % order.length;
}
}
},
updateAll: function(c, resort, callback) {
var table = c.table;
table.isUpdating = true;
ts.refreshWidgets(table, true, true);
ts.buildHeaders(c);
ts.bindEvents(table, c.$headers, true);
ts.bindMethods(c);
ts.commonUpdate(c, resort, callback);
},
update: function(c, resort, callback) {
var table = c.table;
table.isUpdating = true;
// update sorting (if enabled/disabled)
ts.updateHeader(c);
ts.commonUpdate(c, resort, callback);
},
// simple header update - see #989
updateHeaders: function(c, callback) {
c.table.isUpdating = true;
ts.buildHeaders(c);
ts.bindEvents(c.table, c.$headers, true);
ts.resortComplete(c, callback);
},
updateCell: function(c, cell, resort, callback) {
// updateCell for child rows is a mess - we'll ignore them for now
// eventually I'll break out the "update" row cache code to make everything consistent
if ($(cell).closest('tr').hasClass(c.cssChildRow)) {
console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead');
return;
}
if (ts.isEmptyObject(c.cache)) {
// empty table, do an update instead - fixes #1099
ts.updateHeader(c);
ts.commonUpdate(c, resort, callback);
return;
}
c.table.isUpdating = true;
c.$table.find(c.selectorRemove).remove();
// get position from the dom
var tmp, indx, row, icell, cache, len,
$tbodies = c.$tbodies,
$cell = $(cell),
// update cache - format: function( s, table, cell, cellIndex )
// no closest in jQuery v1.2.6
tbodyIndex = $tbodies.index(ts.getClosest($cell, 'tbody')),
tbcache = c.cache[tbodyIndex],
$row = ts.getClosest($cell, 'tr');
cell = $cell[0]; // in case cell is a jQuery object
// tbody may not exist if update is initialized while tbody is removed for processing
if ($tbodies.length && tbodyIndex >= 0) {
row = $tbodies.eq(tbodyIndex).find('tr').not('.' + c.cssChildRow).index($row);
cache = tbcache.normalized[row];
len = $row[0].cells.length;
if (len !== c.columns) {
// colspan in here somewhere!
icell = 0;
tmp = false;
for (indx = 0; indx < len; indx++) {
if (!tmp && $row[0].cells[indx] !== cell) {
icell += $row[0].cells[indx].colSpan;
} else {
tmp = true;
}
}
} else {
icell = $cell.index();
}
tmp = ts.getElementText(c, cell, icell); // raw
cache[c.columns].raw[icell] = tmp;
tmp = ts.getParsedText(c, cell, icell, tmp);
cache[icell] = tmp; // parsed
if ((c.parsers[icell].type || '').toLowerCase() === 'numeric') {
// update column max value (ignore sign)
tbcache.colMax[icell] = Math.max(Math.abs(tmp) || 0, tbcache.colMax[icell] || 0);
}
tmp = resort !== 'undefined' ? resort : c.resort;
if (tmp !== false) {
// widgets will be reapplied
ts.checkResort(c, tmp, callback);
} else {
// don't reapply widgets is resort is false, just in case it causes
// problems with element focus
ts.resortComplete(c, callback);
}
} else {
if (ts.debug(c, 'core')) {
console.error('updateCell aborted, tbody missing or not within the indicated table');
}
c.table.isUpdating = false;
}
},
addRows: function(c, $row, resort, callback) {
var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len, order,
cacheIndex, rowData, cells, cell, span,
// allow passing a row string if only one non-info tbody exists in the table
valid = typeof $row === 'string' && c.$tbodies.length === 1 && /<tr/.test($row || ''),
table = c.table;
if (valid) {
$row = $($row);
c.$tbodies.append($row);
} else if (!$row ||
// row is a jQuery object?
!($row instanceof $) ||
// row contained in the table?
(ts.getClosest($row, 'table')[0] !== c.table)
) {
if (ts.debug(c, 'core')) {
console.error('addRows method requires (1) a jQuery selector reference to rows that have already ' +
'been added to the table, or (2) row HTML string to be added to a table with only one tbody');
}
return false;
}
table.isUpdating = true;
if (ts.isEmptyObject(c.cache)) {
// empty table, do an update instead - fixes #450
ts.updateHeader(c);
ts.commonUpdate(c, resort, callback);
} else {
rows = $row.filter('tr').attr('role', 'row').length;
tbodyIndex = c.$tbodies.index($row.parents('tbody').filter(':first'));
// fixes adding rows to an empty table - see issue #179
if (!(c.parsers && c.parsers.length)) {
ts.setupParsers(c);
}
// add each row
for (rowIndex = 0; rowIndex < rows; rowIndex++) {
cacheIndex = 0;
len = $row[rowIndex].cells.length;
order = c.cache[tbodyIndex].normalized.length;
cells = [];
rowData = {
child: [],
raw: [],
$row: $row.eq(rowIndex),
order: order
};
// add each cell
for (cellIndex = 0; cellIndex < len; cellIndex++) {
cell = $row[rowIndex].cells[cellIndex];
txt = ts.getElementText(c, cell, cacheIndex);
rowData.raw[cacheIndex] = txt;
val = ts.getParsedText(c, cell, cacheIndex, txt);
cells[cacheIndex] = val;
if ((c.parsers[cacheIndex].type || '').toLowerCase() === 'numeric') {
// update column max value (ignore sign)
c.cache[tbodyIndex].colMax[cacheIndex] =
Math.max(Math.abs(val) || 0, c.cache[tbodyIndex].colMax[cacheIndex] || 0);
}
span = cell.colSpan - 1;
if (span > 0) {
cacheIndex += span;
}
cacheIndex++;
}
// add the row data to the end
cells[c.columns] = rowData;
// update cache
c.cache[tbodyIndex].normalized[order] = cells;
}
// resort using current settings
ts.checkResort(c, resort, callback);
}
},
updateCache: function(c, callback, $tbodies) {
// rebuild parsers
if (!(c.parsers && c.parsers.length)) {
ts.setupParsers(c, $tbodies);
}
// rebuild the cache map
ts.buildCache(c, callback, $tbodies);
},
// init flag (true) used by pager plugin to prevent widget application
// renamed from appendToTable
appendCache: function(c, init) {
var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime,
table = c.table,
$tbodies = c.$tbodies,
rows = [],
cache = c.cache;
// empty table - fixes #206/#346
if (ts.isEmptyObject(cache)) {
// run pager appender in case the table was just emptied
return c.appender ? c.appender(table, rows) :
table.isUpdating ? c.$table.triggerHandler('updateComplete', table) : ''; // Fixes #532
}
if (ts.debug(c, 'core')) {
appendTime = new Date();
}
for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++) {
$tbody = $tbodies.eq(tbodyIndex);
if ($tbody.length) {
// detach tbody for manipulation
$curTbody = ts.processTbody(table, $tbody, true);
parsed = cache[tbodyIndex].normalized;
totalRows = parsed.length;
for (rowIndex = 0; rowIndex < totalRows; rowIndex++) {
rows[rows.length] = parsed[rowIndex][c.columns].$row;
// removeRows used by the pager plugin; don't render if using ajax - fixes #411
if (!c.appender || (c.pager && !c.pager.removeRows && !c.pager.ajax)) {
$curTbody.append(parsed[rowIndex][c.columns].$row);
}
}
// restore tbody
ts.processTbody(table, $curTbody, false);
}
}
if (c.appender) {
c.appender(table, rows);
}
if (ts.debug(c, 'core')) {
console.log('Rebuilt table' + ts.benchmark(appendTime));
}
// apply table widgets; but not before ajax completes
if (!init && !c.appender) {
ts.applyWidget(table);
}
if (table.isUpdating) {
c.$table.triggerHandler('updateComplete', table);
}
},
commonUpdate: function(c, resort, callback) {
// remove rows/elements before update
c.$table.find(c.selectorRemove).remove();
// rebuild parsers
ts.setupParsers(c);
// rebuild the cache map
ts.buildCache(c);
ts.checkResort(c, resort, callback);
},
/*
▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄
▀█▄ ██ ██ ██▄▄██ ██ ██ ██ ██ ██ ▄▄▄
▀█▄ ██ ██ ██▀██ ██ ██ ██ ██ ██ ▀██
█████▀ ▀████▀ ██ ██ ██ ██ ██ ██ ▀████▀
*/
initSort: function(c, cell, event) {
if (c.table.isUpdating) {
// let any updates complete before initializing a sort
return setTimeout(function() {
ts.initSort(c, cell, event);
}, 50);
}
var arry, indx, headerIndx, dir, temp, tmp, $header,
notMultiSort = !event[c.sortMultiSortKey],
table = c.table,
len = c.$headers.length,
th = ts.getClosest($(cell), 'th, td'),
col = parseInt(th.attr('data-column'), 10),
sortedBy = event.type === 'mouseup' ? 'user' : event.type,
order = c.sortVars[col].order;
th = th[0];
// Only call sortStart if sorting is enabled
c.$table.triggerHandler('sortStart', table);
// get current column sort order
tmp = (c.sortVars[col].count + 1) % order.length;
c.sortVars[col].count = event[c.sortResetKey] ? 2 : tmp;
// reset all sorts on non-current column - issue #30
if (c.sortRestart) {
for (headerIndx = 0; headerIndx < len; headerIndx++) {
$header = c.$headers.eq(headerIndx);
tmp = parseInt($header.attr('data-column'), 10);
// only reset counts on columns that weren't just clicked on and if not included in a multisort
if (col !== tmp && (notMultiSort || $header.hasClass(ts.css.sortNone))) {
c.sortVars[tmp].count = -1;
}
}
}
// user only wants to sort on one column
if (notMultiSort) {
$.each(c.sortVars, function(i) {
c.sortVars[i].sortedBy = '';
});
// flush the sort list
c.sortList = [];
c.last.sortList = [];
if (c.sortForce !== null) {
arry = c.sortForce;
for (indx = 0; indx < arry.length; indx++) {
if (arry[indx][0] !== col) {
c.sortList[c.sortList.length] = arry[indx];
c.sortVars[arry[indx][0]].sortedBy = 'sortForce';
}
}
}
// add column to sort list
dir = order[c.sortVars[col].count];
if (dir < 2) {
c.sortList[c.sortList.length] = [col, dir];
c.sortVars[col].sortedBy = sortedBy;
// add other columns if header spans across multiple
if (th.colSpan > 1) {
for (indx = 1; indx < th.colSpan; indx++) {
c.sortList[c.sortList.length] = [col + indx, dir];
// update count on columns in colSpan
c.sortVars[col + indx].count = $.inArray(dir, order);
c.sortVars[col + indx].sortedBy = sortedBy;
}
}
}
// multi column sorting
} else {
// get rid of the sortAppend before adding more - fixes issue #115 & #523
c.sortList = $.extend([], c.last.sortList);
// the user has clicked on an already sorted column
if (ts.isValueInArray(col, c.sortList) >= 0) {
// reverse the sorting direction
c.sortVars[col].sortedBy = sortedBy;
for (indx = 0; indx < c.sortList.length; indx++) {
tmp = c.sortList[indx];
if (tmp[0] === col) {
// order.count seems to be incorrect when compared to cell.count
tmp[1] = order[c.sortVars[col].count];
if (tmp[1] === 2) {
c.sortList.splice(indx, 1);
c.sortVars[col].count = -1;
}
}
}
} else {
// add column to sort list array
dir = order[c.sortVars[col].count];
c.sortVars[col].sortedBy = sortedBy;
if (dir < 2) {
c.sortList[c.sortList.length] = [col, dir];
// add other columns if header spans across multiple
if (th.colSpan > 1) {
for (indx = 1; indx < th.colSpan; indx++) {
c.sortList[c.sortList.length] = [col + indx, dir];
// update count on columns in colSpan
c.sortVars[col + indx].count = $.inArray(dir, order);
c.sortVars[col + indx].sortedBy = sortedBy;
}
}
}
}
}
// save sort before applying sortAppend
c.last.sortList = $.extend([], c.sortList);
if (c.sortList.length && c.sortAppend) {
arry = $.isArray(c.sortAppend) ? c.sortAppend : c.sortAppend[c.sortList[0][0]];
if (!ts.isEmptyObject(arry)) {
for (indx = 0; indx < arry.length; indx++) {
if (arry[indx][0] !== col && ts.isValueInArray(arry[indx][0], c.sortList) < 0) {
dir = arry[indx][1];
temp = ('' + dir).match(/^(a|d|s|o|n)/);
if (temp) {
tmp = c.sortList[0][1];
switch (temp[0]) {
case 'd':
dir = 1;
break;
case 's':
dir = tmp;
break;
case 'o':
dir = tmp === 0 ? 1 : 0;
break;
case 'n':
dir = (tmp + 1) % order.length;
break;
default:
dir = 0;
break;
}
}
c.sortList[c.sortList.length] = [arry[indx][0], dir];
c.sortVars[arry[indx][0]].sortedBy = 'sortAppend';
}
}
}
}
// sortBegin event triggered immediately before the sort
c.$table.triggerHandler('sortBegin', table);
// setTimeout needed so the processing icon shows up
setTimeout(function() {
// set css for headers
ts.setHeadersCss(c);
ts.multisort(c);
ts.appendCache(c);
c.$table.triggerHandler('sortBeforeEnd', table);
c.$table.triggerHandler('sortEnd', table);
}, 1);
},
// sort multiple columns
multisort: function(c) { /*jshint loopfunc:true */
var tbodyIndex, sortTime, colMax, rows, tmp,
table = c.table,
sorter = [],
dir = 0,
textSorter = c.textSorter || '',
sortList = c.sortList,
sortLen = sortList.length,
len = c.$tbodies.length;
if (c.serverSideSorting || ts.isEmptyObject(c.cache)) {
// empty table - fixes #206/#346
return;
}
if (ts.debug(c, 'core')) {
sortTime = new Date();
}
// cache textSorter to optimize speed
if (typeof textSorter === 'object') {
colMax = c.columns;
while (colMax--) {
tmp = ts.getColumnData(table, textSorter, colMax);
if (typeof tmp === 'function') {
sorter[colMax] = tmp;
}
}
}
for (tbodyIndex = 0; tbodyIndex < len; tbodyIndex++) {
colMax = c.cache[tbodyIndex].colMax;
rows = c.cache[tbodyIndex].normalized;
rows.sort(function(a, b) {
var sortIndex, num, col, order, sort, x, y;
// rows is undefined here in IE, so don't use it!
for (sortIndex = 0; sortIndex < sortLen; sortIndex++) {
col = sortList[sortIndex][0];
order = sortList[sortIndex][1];
// sort direction, true = asc, false = desc
dir = order === 0;
if (c.sortStable && a[col] === b[col] && sortLen === 1) {
return a[c.columns].order - b[c.columns].order;
}
// fallback to natural sort since it is more robust
num = /n/i.test(ts.getSortType(c.parsers, col));
if (num && c.strings[col]) {
// sort strings in numerical columns
if (typeof(ts.string[c.strings[col]]) === 'boolean') {
num = (dir ? 1 : -1) * (ts.string[c.strings[col]] ? -1 : 1);
} else {
num = (c.strings[col]) ? ts.string[c.strings[col]] || 0 : 0;
}
// fall back to built-in numeric sort
// var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table );
sort = c.numberSorter ? c.numberSorter(a[col], b[col], dir, colMax[col], table) :
ts['sortNumeric' + (dir ? 'Asc' : 'Desc')](a[col], b[col], num, colMax[col], col, c);
} else {
// set a & b depending on sort direction
x = dir ? a : b;
y = dir ? b : a;
// text sort function
if (typeof textSorter === 'function') {
// custom OVERALL text sorter
sort = textSorter(x[col], y[col], dir, col, table);
} else if (typeof sorter[col] === 'function') {
// custom text sorter for a SPECIFIC COLUMN
sort = sorter[col](x[col], y[col], dir, col, table);
} else {
// fall back to natural sort
sort = ts['sortNatural' + (dir ? 'Asc' : 'Desc')](a[col] || '', b[col] || '', col, c);
}
}
if (sort) {
return sort;
}
}
return a[c.columns].order - b[c.columns].order;
});
}
if (ts.debug(c, 'core')) {
console.log('Applying sort ' + sortList.toString() + ts.benchmark(sortTime));
}
},
resortComplete: function(c, callback) {
if (c.table.isUpdating) {
c.$table.triggerHandler('updateComplete', c.table);
}
if ($.isFunction(callback)) {
callback(c.table);
}
},
checkResort: function(c, resort, callback) {
var sortList = $.isArray(resort) ? resort : c.sortList,
// if no resort parameter is passed, fallback to config.resort (true by default)
resrt = typeof resort === 'undefined' ? c.resort : resort;
// don't try to resort if the table is still processing
// this will catch spamming of the updateCell method
if (resrt !== false && !c.serverSideSorting && !c.table.isProcessing) {
if (sortList.length) {
ts.sortOn(c, sortList, function() {
ts.resortComplete(c, callback);
}, true);
} else {
ts.sortReset(c, function() {
ts.resortComplete(c, callback);
ts.applyWidget(c.table, false);
});
}
} else {
ts.resortComplete(c, callback);
ts.applyWidget(c.table, false);
}
},
sortOn: function(c, list, callback, init) {
var indx,
table = c.table;
c.$table.triggerHandler('sortStart', table);
for (indx = 0; indx < c.columns; indx++) {
c.sortVars[indx].sortedBy = ts.isValueInArray(indx, list) > -1 ? 'sorton' : '';
}
// update header count index
ts.updateHeaderSortCount(c, list);
// set css for headers
ts.setHeadersCss(c);
// fixes #346
if (c.delayInit && ts.isEmptyObject(c.cache)) {
ts.buildCache(c);
}
c.$table.triggerHandler('sortBegin', table);
// sort the table and append it to the dom
ts.multisort(c);
ts.appendCache(c, init);
c.$table.triggerHandler('sortBeforeEnd', table);
c.$table.triggerHandler('sortEnd', table);
ts.applyWidget(table);
if ($.isFunction(callback)) {
callback(table);
}
},
sortReset: function(c, callback) {
c.sortList = [];
var indx;
for (indx = 0; indx < c.columns; indx++) {
c.sortVars[indx].count = -1;
c.sortVars[indx].sortedBy = '';
}
ts.setHeadersCss(c);
ts.multisort(c);
ts.appendCache(c);
if ($.isFunction(callback)) {
callback(c.table);
}
},
getSortType: function(parsers, column) {
return (parsers && parsers[column]) ? parsers[column].type || '' : '';
},
getOrder: function(val) {
// look for 'd' in 'desc' order; return true
return (/^d/i.test(val) || val === 1);
},
// Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed)
sortNatural: function(a, b) {
if (a === b) {
return 0;
}
a = (a || '').toString();
b = (b || '').toString();
var aNum, bNum, aFloat, bFloat, indx, max,
regex = ts.regex;
// first try and sort Hex codes
if (regex.hex.test(b)) {
aNum = parseInt(a.match(regex.hex), 16);
bNum = parseInt(b.match(regex.hex), 16);
if (aNum < bNum) {
return -1;
}
if (aNum > bNum) {
return 1;
}
}
// chunk/tokenize
aNum = a.replace(regex.chunk, '\\0$1\\0').replace(regex.chunks, '').split('\\0');
bNum = b.replace(regex.chunk, '\\0$1\\0').replace(regex.chunks, '').split('\\0');
max = Math.max(aNum.length, bNum.length);
// natural sorting through split numeric strings and default strings
for (indx = 0; indx < max; indx++) {
// find floats not starting with '0', string or 0 if not defined
aFloat = isNaN(aNum[indx]) ? aNum[indx] || 0 : parseFloat(aNum[indx]) || 0;
bFloat = isNaN(bNum[indx]) ? bNum[indx] || 0 : parseFloat(bNum[indx]) || 0;
// handle numeric vs string comparison - number < string - (Kyle Adams)
if (isNaN(aFloat) !== isNaN(bFloat)) {
return isNaN(aFloat) ? 1 : -1;
}
// rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
if (typeof aFloat !== typeof bFloat) {
aFloat += '';
bFloat += '';
}
if (aFloat < bFloat) {
return -1;
}
if (aFloat > bFloat) {
return 1;
}
}
return 0;
},
sortNaturalAsc: function(a, b, col, c) {
if (a === b) {
return 0;
}
var empty = ts.string[(c.empties[col] || c.emptyTo)];
if (a === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? -1 : 1) : -empty || -1;
}
if (b === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? 1 : -1) : empty || 1;
}
return ts.sortNatural(a, b);
},
sortNaturalDesc: function(a, b, col, c) {
if (a === b) {
return 0;
}
var empty = ts.string[(c.empties[col] || c.emptyTo)];
if (a === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? -1 : 1) : empty || 1;
}
if (b === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? 1 : -1) : -empty || -1;
}
return ts.sortNatural(b, a);
},
// basic alphabetical sort
sortText: function(a, b) {
return a > b ? 1 : (a < b ? -1 : 0);
},
// return text string value by adding up ascii value
// so the text is somewhat sorted when using a digital sort
// this is NOT an alphanumeric sort
getTextValue: function(val, num, max) {
if (max) {
// make sure the text value is greater than the max numerical value (max)
var indx,
len = val ? val.length : 0,
n = max + num;
for (indx = 0; indx < len; indx++) {
n += val.charCodeAt(indx);
}
return num * n;
}
return 0;
},
sortNumericAsc: function(a, b, num, max, col, c) {
if (a === b) {
return 0;
}
var empty = ts.string[(c.empties[col] || c.emptyTo)];
if (a === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? -1 : 1) : -empty || -1;
}
if (b === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? 1 : -1) : empty || 1;
}
if (isNaN(a)) {
a = ts.getTextValue(a, num, max);
}
if (isNaN(b)) {
b = ts.getTextValue(b, num, max);
}
return a - b;
},
sortNumericDesc: function(a, b, num, max, col, c) {
if (a === b) {
return 0;
}
var empty = ts.string[(c.empties[col] || c.emptyTo)];
if (a === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? -1 : 1) : empty || 1;
}
if (b === '' && empty !== 0) {
return typeof empty === 'boolean' ? (empty ? 1 : -1) : -empty || -1;
}
if (isNaN(a)) {
a = ts.getTextValue(a, num, max);
}
if (isNaN(b)) {
b = ts.getTextValue(b, num, max);
}
return b - a;
},
sortNumeric: function(a, b) {
return a - b;
},
/*
██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████
██ ██ ██ ██ ██ ██ ██ ▄▄▄ ██▄▄ ██ ▀█▄
██ ██ ██ ██ ██ ██ ██ ▀██ ██▀▀ ██ ▀█▄
███████▀ ██ █████▀ ▀████▀ ██████ ██ █████▀
*/
addWidget: function(widget) {
if (widget.id && !ts.isEmptyObject(ts.getWidgetById(widget.id))) {
console.warn('"' + widget.id + '" widget was loaded more than once!');
}
ts.widgets[ts.widgets.length] = widget;
},
hasWidget: function($table, name) {
$table = $($table);
return $table.length && $table[0].config && $table[0].config.widgetInit[name] || false;
},
getWidgetById: function(name) {
var indx, widget,
len = ts.widgets.length;
for (indx = 0; indx < len; indx++) {
widget = ts.widgets[indx];
if (widget && widget.id && widget.id.toLowerCase() === name.toLowerCase()) {
return widget;
}
}
},
applyWidgetOptions: function(table) {
var indx, widget, wo,
c = table.config,
len = c.widgets.length;
if (len) {
for (indx = 0; indx < len; indx++) {
widget = ts.getWidgetById(c.widgets[indx]);
if (widget && widget.options) {
wo = $.extend(true, {}, widget.options);
c.widgetOptions = $.extend(true, wo, c.widgetOptions);
// add widgetOptions to defaults for option validator
$.extend(true, ts.defaults.widgetOptions, widget.options);
}
}
}
},
addWidgetFromClass: function(table) {
var len, indx,
c = table.config,
// look for widgets to apply from table class
// don't match from 'ui-widget-content'; use \S instead of \w to include widgets
// with dashes in the name, e.g. "widget-test-2" extracts out "test-2"
regex = '^' + c.widgetClass.replace(ts.regex.templateName, '(\\S+)+') + '$',
widgetClass = new RegExp(regex, 'g'),
// split up table class (widget id's can include dashes) - stop using match
// otherwise only one widget gets extracted, see #1109
widgets = (table.className || '').split(ts.regex.spaces);
if (widgets.length) {
len = widgets.length;
for (indx = 0; indx < len; indx++) {
if (widgets[indx].match(widgetClass)) {
c.widgets[c.widgets.length] = widgets[indx].replace(widgetClass, '$1');
}
}
}
},
applyWidgetId: function(table, id, init) {
table = $(table)[0];
var applied, time, name,
c = table.config,
wo = c.widgetOptions,
debug = ts.debug(c, 'core'),
widget = ts.getWidgetById(id);
if (widget) {
name = widget.id;
applied = false;
// add widget name to option list so it gets reapplied after sorting, filtering, etc
if ($.inArray(name, c.widgets) < 0) {
c.widgets[c.widgets.length] = name;
}
if (debug) {
time = new Date();
}
if (init || !(c.widgetInit[name])) {
// set init flag first to prevent calling init more than once (e.g. pager)
c.widgetInit[name] = true;
if (table.hasInitialized) {
// don't reapply widget options on tablesorter init
ts.applyWidgetOptions(table);
}
if (typeof widget.init === 'function') {
applied = true;
if (debug) {
console[console.group ? 'group' : 'log']('Initializing ' + name + ' widget');
}
widget.init(table, widget, c, wo);
}
}
if (!init && typeof widget.format === 'function') {
applied = true;
if (debug) {
console[console.group ? 'group' : 'log']('Updating ' + name + ' widget');
}
widget.format(table, c, wo, false);
}
if (debug) {
if (applied) {
console.log('Completed ' + (init ? 'initializing ' : 'applying ') + name + ' widget' + ts.benchmark(time));
if (console.groupEnd) {
console.groupEnd();
}
}
}
}
},
applyWidget: function(table, init, callback) {
table = $(table)[0]; // in case this is called externally
var indx, len, names, widget, time,
c = table.config,
debug = ts.debug(c, 'core'),
widgets = [];
// prevent numerous consecutive widget applications
if (init !== false && table.hasInitialized && (table.isApplyingWidgets || table.isUpdating)) {
return;
}
if (debug) {
time = new Date();
}
ts.addWidgetFromClass(table);
// prevent "tablesorter-ready" from firing multiple times in a row
clearTimeout(c.timerReady);
if (c.widgets.length) {
table.isApplyingWidgets = true;
// ensure unique widget ids
c.widgets = $.grep(c.widgets, function(val, index) {
return $.inArray(val, c.widgets) === index;
});
names = c.widgets || [];
len = names.length;
// build widget array & add priority as needed
for (indx = 0; indx < len; indx++) {
widget = ts.getWidgetById(names[indx]);
if (widget && widget.id) {
// set priority to 10 if not defined
if (!widget.priority) {
widget.priority = 10;
}
widgets[indx] = widget;
} else if (debug) {
console.warn('"' + names[indx] + '" was enabled, but the widget code has not been loaded!');
}
}
// sort widgets by priority
widgets.sort(function(a, b) {
return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1;
});
// add/update selected widgets
len = widgets.length;
if (debug) {
console[console.group ? 'group' : 'log']('Start ' + (init ? 'initializing' : 'applying') + ' widgets');
}
for (indx = 0; indx < len; indx++) {
widget = widgets[indx];
if (widget && widget.id) {
ts.applyWidgetId(table, widget.id, init);
}
}
if (debug && console.groupEnd) {
console.groupEnd();
}
}
c.timerReady = setTimeout(function() {
table.isApplyingWidgets = false;
$.data(table, 'lastWidgetApplication', new Date());
c.$table.triggerHandler('tablesorter-ready');
// callback executed on init only
if (!init && typeof callback === 'function') {
callback(table);
}
if (debug) {
widget = c.widgets.length;
console.log('Completed ' +
(init === true ? 'initializing ' : 'applying ') + widget +
' widget' + (widget !== 1 ? 's' : '') + ts.benchmark(time));
}
}, 10);
},
removeWidget: function(table, name, refreshing) {
table = $(table)[0];
var index, widget, indx, len,
c = table.config;
// if name === true, add all widgets from $.tablesorter.widgets
if (name === true) {
name = [];
len = ts.widgets.length;
for (indx = 0; indx < len; indx++) {
widget = ts.widgets[indx];
if (widget && widget.id) {
name[name.length] = widget.id;
}
}
} else {
// name can be either an array of widgets names,
// or a space/comma separated list of widget names
name = ($.isArray(name) ? name.join(',') : name || '').toLowerCase().split(/[\s,]+/);
}
len = name.length;
for (index = 0; index < len; index++) {
widget = ts.getWidgetById(name[index]);
indx = $.inArray(name[index], c.widgets);
// don't remove the widget from config.widget if refreshing
if (indx >= 0 && refreshing !== true) {
c.widgets.splice(indx, 1);
}
if (widget && widget.remove) {
if (ts.debug(c, 'core')) {
console.log((refreshing ? 'Refreshing' : 'Removing') + ' "' + name[index] + '" widget');
}
widget.remove(table, c, c.widgetOptions, refreshing);
c.widgetInit[name[index]] = false;
}
}
c.$table.triggerHandler('widgetRemoveEnd', table);
},
refreshWidgets: function(table, doAll, dontapply) {
table = $(table)[0]; // see issue #243
var indx, widget,
c = table.config,
curWidgets = c.widgets,
widgets = ts.widgets,
len = widgets.length,
list = [],
callback = function(table) {
$(table).triggerHandler('refreshComplete');
};
// remove widgets not defined in config.widgets, unless doAll is true
for (indx = 0; indx < len; indx++) {
widget = widgets[indx];
if (widget && widget.id && (doAll || $.inArray(widget.id, curWidgets) < 0)) {
list[list.length] = widget.id;
}
}
ts.removeWidget(table, list.join(','), true);
if (dontapply !== true) {
// call widget init if
ts.applyWidget(table, doAll || false, callback);
if (doAll) {
// apply widget format
ts.applyWidget(table, false, callback);
}
} else {
callback(table);
}
},
/*
██ ██ ██████ ██ ██ ██ ██████ ██ ██████ ▄█████
██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ▀█▄
██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀█▄
▀████▀ ██ ██ ██████ ██ ██ ██ ██████ █████▀
*/
benchmark: function(diff) {
return (' (' + (new Date().getTime() - diff.getTime()) + ' ms)');
},
// deprecated ts.log
log: function() {
console.log(arguments);
},
debug: function(c, name) {
return c && (
c.debug === true ||
typeof c.debug === 'string' && c.debug.indexOf(name) > -1
);
},
// $.isEmptyObject from jQuery v1.4
isEmptyObject: function(obj) {
/*jshint forin: false */
for (var name in obj) {
return false;
}
return true;
},
isValueInArray: function(column, arry) {
var indx,
len = arry && arry.length || 0;
for (indx = 0; indx < len; indx++) {
if (arry[indx][0] === column) {
return indx;
}
}
return -1;
},
formatFloat: function(str, table) {
if (typeof str !== 'string' || str === '') {
return str;
}
// allow using formatFloat without a table; defaults to US number format
var num,
usFormat = table && table.config ? table.config.usNumberFormat !== false :
typeof table !== 'undefined' ? table : true;
if (usFormat) {
// US Format - 1,234,567.89 -> 1234567.89
str = str.replace(ts.regex.comma, '');
} else {
// German Format = 1.234.567,89 -> 1234567.89
// French Format = 1 234 567,89 -> 1234567.89
str = str.replace(ts.regex.digitNonUS, '').replace(ts.regex.comma, '.');
}
if (ts.regex.digitNegativeTest.test(str)) {
// make (#) into a negative number -> (10) = -10
str = str.replace(ts.regex.digitNegativeReplace, '-$1');
}
num = parseFloat(str);
// return the text instead of zero
return isNaN(num) ? $.trim(str) : num;
},
isDigit: function(str) {
// replace all unwanted chars and match
return isNaN(str) ?
ts.regex.digitTest.test(str.toString().replace(ts.regex.digitReplace, '')) :
str !== '';
},
// computeTableHeaderCellIndexes from:
// http://www.javascripttoolbox.com/lib/table/examples.php
// http://www.javascripttoolbox.com/temp/table_cellindex.html
computeColumnIndex: function($rows, c) {
var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol,
// total columns has been calculated, use it to set the matrixrow
columns = c && c.columns || 0,
matrix = [],
matrixrow = new Array(columns);
for (i = 0; i < $rows.length; i++) {
cells = $rows[i].cells;
for (j = 0; j < cells.length; j++) {
cell = cells[j];
rowIndex = i;
rowSpan = cell.rowSpan || 1;
colSpan = cell.colSpan || 1;
if (typeof matrix[rowIndex] === 'undefined') {
matrix[rowIndex] = [];
}
// Find first available column in the first row
for (k = 0; k < matrix[rowIndex].length + 1; k++) {
if (typeof matrix[rowIndex][k] === 'undefined') {
firstAvailCol = k;
break;
}
}
// jscs:disable disallowEmptyBlocks
if (columns && cell.cellIndex === firstAvailCol) {
// don't to anything
} else if (cell.setAttribute) {
// jscs:enable disallowEmptyBlocks
// add data-column (setAttribute = IE8+)
cell.setAttribute('data-column', firstAvailCol);
} else {
// remove once we drop support for IE7 - 1/12/2016
$(cell).attr('data-column', firstAvailCol);
}
for (k = rowIndex; k < rowIndex + rowSpan; k++) {
if (typeof matrix[k] === 'undefined') {
matrix[k] = [];
}
matrixrow = matrix[k];
for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) {
matrixrow[l] = 'x';
}
}
}
}
ts.checkColumnCount($rows, matrix, matrixrow.length);
return matrixrow.length;
},
checkColumnCount: function($rows, matrix, columns) {
// this DOES NOT report any tbody column issues, except for the math and
// and column selector widgets
var i, len,
valid = true,
cells = [];
for (i = 0; i < matrix.length; i++) {
// some matrix entries are undefined when testing the footer because
// it is using the rowIndex property
if (matrix[i]) {
len = matrix[i].length;
if (matrix[i].length !== columns) {
valid = false;
break;
}
}
}
if (!valid) {
$rows.each(function(indx, el) {
var cell = el.parentElement.nodeName;
if (cells.indexOf(cell) < 0) {
cells.push(cell);
}
});
console.error(
'Invalid or incorrect number of columns in the ' +
cells.join(' or ') + '; expected ' + columns +
', but found ' + len + ' columns'
);
}
},
// automatically add a colgroup with col elements set to a percentage width
fixColumnWidth: function(table) {
table = $(table)[0];
var overallWidth, percent, $tbodies, len, index,
c = table.config,
$colgroup = c.$table.children('colgroup');
// remove plugin-added colgroup, in case we need to refresh the widths
if ($colgroup.length && $colgroup.hasClass(ts.css.colgroup)) {
$colgroup.remove();
}
if (c.widthFixed && c.$table.children('colgroup').length === 0) {
$colgroup = $('<colgroup class="' + ts.css.colgroup + '">');
overallWidth = c.$table.width();
// only add col for visible columns - fixes #371
$tbodies = c.$tbodies.find('tr:first').children(':visible');
len = $tbodies.length;
for (index = 0; index < len; index++) {
percent = parseInt(($tbodies.eq(index).width() / overallWidth) * 1000, 10) / 10 + '%';
$colgroup.append($('<col>').css('width', percent));
}
c.$table.prepend($colgroup);
}
},
// get sorter, string, empty, etc options for each column from
// jQuery data, metadata, header option or header class name ('sorter-false')
// priority = jQuery data > meta > headers option > header class name
getData: function(header, configHeader, key) {
var meta, cl4ss,
val = '',
$header = $(header);
if (!$header.length) {
return '';
}
meta = $.metadata ? $header.metadata() : false;
cl4ss = ' ' + ($header.attr('class') || '');
if (typeof $header.data(key) !== 'undefined' ||
typeof $header.data(key.toLowerCase()) !== 'undefined') {
// 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder'
// 'data-sort-initial-order' is assigned to 'sortInitialOrder'
val += $header.data(key) || $header.data(key.toLowerCase());
} else if (meta && typeof meta[key] !== 'undefined') {
val += meta[key];
} else if (configHeader && typeof configHeader[key] !== 'undefined') {
val += configHeader[key];
} else if (cl4ss !== ' ' && cl4ss.match(' ' + key + '-')) {
// include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser'
val = cl4ss.match(new RegExp('\\s' + key + '-([\\w-]+)'))[1] || '';
}
return $.trim(val);
},
getColumnData: function(table, obj, indx, getCell, $headers) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
table = $(table)[0];
var $header, key,
c = table.config,
$cells = ($headers || c.$headers),
// c.$headerIndexed is not defined initially
$cell = c.$headerIndexed && c.$headerIndexed[indx] ||
$cells.find('[data-column="' + indx + '"]:last');
if (typeof obj[indx] !== 'undefined') {
return getCell ? obj[indx] : obj[$cells.index($cell)];
}
for (key in obj) {
if (typeof key === 'string') {
$header = $cell
// header cell with class/id
.filter(key)
// find elements within the header cell with cell/id
.add($cell.find(key));
if ($header.length) {
return obj[key];
}
}
}
return;
},
// *** Process table ***
// add processing indicator
isProcessing: function($table, toggle, $headers) {
$table = $($table);
var c = $table[0].config,
// default to all headers
$header = $headers || $table.find('.' + ts.css.header);
if (toggle) {
// don't use sortList if custom $headers used
if (typeof $headers !== 'undefined' && c.sortList.length > 0) {
// get headers from the sortList
$header = $header.filter(function() {
// get data-column from attr to keep compatibility with jQuery 1.2.6
return this.sortDisabled ?
false :
ts.isValueInArray(parseFloat($(this).attr('data-column')), c.sortList) >= 0;
});
}
$table.add($header).addClass(ts.css.processing + ' ' + c.cssProcessing);
} else {
$table.add($header).removeClass(ts.css.processing + ' ' + c.cssProcessing);
}
},
// detach tbody but save the position
// don't use tbody because there are portions that look for a tbody index (updateCell)
processTbody: function(table, $tb, getIt) {
table = $(table)[0];
if (getIt) {
table.isProcessing = true;
$tb.before('<colgroup class="tablesorter-savemyplace"/>');
return $.fn.detach ? $tb.detach() : $tb.remove();
}
var holdr = $(table).find('colgroup.tablesorter-savemyplace');
$tb.insertAfter(holdr);
holdr.remove();
table.isProcessing = false;
},
clearTableBody: function(table) {
$(table)[0].config.$tbodies.children().detach();
},
// used when replacing accented characters during sorting
characterEquivalents: {
'a': '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
'A': '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
'c': '\u00e7\u0107\u010d', // çćč
'C': '\u00c7\u0106\u010c', // ÇĆČ
'e': '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
'E': '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
'i': '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
'I': '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
'o': '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō
'O': '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ
'ss': '\u00df', // ß (s sharp)
'SS': '\u1e9e', // ẞ (Capital sharp s)
'u': '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
'U': '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
},
replaceAccents: function(str) {
var chr,
acc = '[',
eq = ts.characterEquivalents;
if (!ts.characterRegex) {
ts.characterRegexArray = {};
for (chr in eq) {
if (typeof chr === 'string') {
acc += eq[chr];
ts.characterRegexArray[chr] = new RegExp('[' + eq[chr] + ']', 'g');
}
}
ts.characterRegex = new RegExp(acc + ']');
}
if (ts.characterRegex.test(str)) {
for (chr in eq) {
if (typeof chr === 'string') {
str = str.replace(ts.characterRegexArray[chr], chr);
}
}
}
return str;
},
validateOptions: function(c) {
var setting, setting2, typ, timer,
// ignore options containing an array
ignore = 'headers sortForce sortList sortAppend widgets'.split(' '),
orig = c.originalSettings;
if (orig) {
if (ts.debug(c, 'core')) {
timer = new Date();
}
for (setting in orig) {
typ = typeof ts.defaults[setting];
if (typ === 'undefined') {
console.warn('Tablesorter Warning! "table.config.' + setting + '" option not recognized');
} else if (typ === 'object') {
for (setting2 in orig[setting]) {
typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2];
if ($.inArray(setting, ignore) < 0 && typ === 'undefined') {
console.warn('Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized');
}
}
}
}
if (ts.debug(c, 'core')) {
console.log('validate options time:' + ts.benchmark(timer));
}
}
},
// restore headers
restoreHeaders: function(table) {
var index, $cell,
c = $(table)[0].config,
$headers = c.$table.find(c.selectorHeaders),
len = $headers.length;
// don't use c.$headers here in case header cells were swapped
for (index = 0; index < len; index++) {
$cell = $headers.eq(index);
// only restore header cells if it is wrapped
// because this is also used by the updateAll method
if ($cell.find('.' + ts.css.headerIn).length) {
$cell.html(c.headerContent[index]);
}
}
},
destroy: function(table, removeClasses, callback) {
table = $(table)[0];
if (!table.hasInitialized) {
return;
}
// remove all widgets
ts.removeWidget(table, true, false);
var events,
$t = $(table),
c = table.config,
$h = $t.find('thead:first'),
$r = $h.find('tr.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow),
$f = $t.find('tfoot:first > tr').children('th, td');
if (removeClasses === false && $.inArray('uitheme', c.widgets) >= 0) {
// reapply uitheme classes, in case we want to maintain appearance
$t.triggerHandler('applyWidgetId', ['uitheme']);
$t.triggerHandler('applyWidgetId', ['zebra']);
}
// remove widget added rows, just in case
$h.find('tr').not($r).remove();
// disable tablesorter - not using .unbind( namespace ) because namespacing was
// added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/
events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' +
'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' +
'keypress sortBegin sortEnd resetToLoadState '.split(' ')
.join(c.namespace + ' ');
$t
.removeData('tablesorter')
.unbind(events.replace(ts.regex.spaces, ' '));
c.$headers
.add($f)
.removeClass([ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone].join(' '))
.removeAttr('data-column')
.removeAttr('aria-label')
.attr('aria-disabled', 'true');
$r
.find(c.selectorSort)
.unbind(('mousedown mouseup keypress '.split(' ').join(c.namespace + ' ')).replace(ts.regex.spaces, ' '));
ts.restoreHeaders(table);
$t.toggleClass(ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false);
$t.removeClass(c.namespace.slice(1));
// clear flag in case the plugin is initialized again
table.hasInitialized = false;
delete table.config.cache;
if (typeof callback === 'function') {
callback(table);
}
if (ts.debug(c, 'core')) {
console.log('tablesorter has been removed');
}
}
};
$.fn.tablesorter = function(settings) {
return this.each(function() {
var table = this,
// merge & extend config options
c = $.extend(true, {}, ts.defaults, settings, ts.instanceMethods);
// save initial settings
c.originalSettings = settings;
// create a table from data (build table widget)
if (!table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE') {
// return the table (in case the original target is the table's container)
ts.buildTable(table, c);
} else {
ts.setup(table, c);
}
});
};
// set up debug logs
if (!(window.console && window.console.log)) {
// access $.tablesorter.logs for browsers that don't have a console...
ts.logs = [];
/*jshint -W020 */
console = {};
console.log = console.warn = console.error = console.table = function() {
var arg = arguments.length > 1 ? arguments : arguments[0];
ts.logs[ts.logs.length] = {
date: Date.now(),
log: arg
};
};
}
// add default parsers
ts.addParser({
id: 'no-parser',
is: function() {
return false;
},
format: function() {
return '';
},
type: 'text'
});
ts.addParser({
id: 'text',
is: function() {
return true;
},
format: function(str, table) {
var c = table.config;
if (str) {
str = $.trim(c.ignoreCase ? str.toLocaleLowerCase() : str);
str = c.sortLocaleCompare ? ts.replaceAccents(str) : str;
}
return str;
},
type: 'text'
});
ts.regex.nondigit = /[^\w,. \-()]/g;
ts.addParser({
id: 'digit',
is: function(str) {
return ts.isDigit(str);
},
format: function(str, table) {
var num = ts.formatFloat((str || '').replace(ts.regex.nondigit, ''), table);
return str && typeof num === 'number' ? num :
str ? $.trim(str && table.config.ignoreCase ? str.toLocaleLowerCase() : str) : str;
},
type: 'numeric'
});
ts.regex.currencyReplace = /[+\-,. ]/g;
ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/;
ts.addParser({
id: 'currency',
is: function(str) {
str = (str || '').replace(ts.regex.currencyReplace, '');
// test for £$€¤¥¢
return ts.regex.currencyTest.test(str);
},
format: function(str, table) {
var num = ts.formatFloat((str || '').replace(ts.regex.nondigit, ''), table);
return str && typeof num === 'number' ? num :
str ? $.trim(str && table.config.ignoreCase ? str.toLocaleLowerCase() : str) : str;
},
type: 'numeric'
});
// too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme
// now, this regex can be updated before initialization
ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//;
ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/;
ts.addParser({
id: 'url',
is: function(str) {
return ts.regex.urlProtocolTest.test(str);
},
format: function(str) {
return str ? $.trim(str.replace(ts.regex.urlProtocolReplace, '')) : str;
},
type: 'text'
});
ts.regex.dash = /-/g;
ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/;
ts.addParser({
id: 'isoDate',
is: function(str) {
return ts.regex.isoDate.test(str);
},
format: function(str) {
var date = str ? new Date(str.replace(ts.regex.dash, '/')) : str;
return date instanceof Date && isFinite(date) ? date.getTime() : str;
},
type: 'numeric'
});
ts.regex.percent = /%/g;
ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/;
ts.addParser({
id: 'percent',
is: function(str) {
return ts.regex.percentTest.test(str) && str.length < 15;
},
format: function(str, table) {
return str ? ts.formatFloat(str.replace(ts.regex.percent, ''), table) : str;
},
type: 'numeric'
});
// added image parser to core v2.17.9
ts.addParser({
id: 'image',
is: function(str, table, node, $node) {
return $node.find('img').length > 0;
},
format: function(str, table, cell) {
return $(cell).find('img').attr(table.config.imgAttr || 'alt') || str;
},
parsed: true, // filter widget flag
type: 'text'
});
ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser
ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i;
ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i;
ts.addParser({
id: 'usLongDate',
is: function(str) {
// two digit years are not allowed cross-browser
// Jan 01, 2013 12:34:56 PM or 01 Jan 2013
return ts.regex.usLongDateTest1.test(str) || ts.regex.usLongDateTest2.test(str);
},
format: function(str) {
var date = str ? new Date(str.replace(ts.regex.dateReplace, '$1 $2')) : str;
return date instanceof Date && isFinite(date) ? date.getTime() : str;
},
type: 'numeric'
});
// testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included
ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/;
// escaped "-" because JSHint in Firefox was showing it as an error
ts.regex.shortDateReplace = /[\-.,]/g;
// XXY covers MDY & DMY formats
ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/;
ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/;
ts.convertFormat = function(dateString, format) {
dateString = (dateString || '')
.replace(ts.regex.spaces, ' ')
.replace(ts.regex.shortDateReplace, '/');
if (format === 'mmddyyyy') {
dateString = dateString.replace(ts.regex.shortDateXXY, '$3/$1/$2');
} else if (format === 'ddmmyyyy') {
dateString = dateString.replace(ts.regex.shortDateXXY, '$3/$2/$1');
} else if (format === 'yyyymmdd') {
dateString = dateString.replace(ts.regex.shortDateYMD, '$1/$2/$3');
}
var date = new Date(dateString);
return date instanceof Date && isFinite(date) ? date.getTime() : '';
};
ts.addParser({
id: 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd'
is: function(str) {
str = (str || '').replace(ts.regex.spaces, ' ').replace(ts.regex.shortDateReplace, '/');
return ts.regex.shortDateTest.test(str);
},
format: function(str, table, cell, cellIndex) {
if (str) {
var c = table.config,
$header = c.$headerIndexed[cellIndex],
format = $header.length && $header.data('dateFormat') ||
ts.getData($header, ts.getColumnData(table, c.headers, cellIndex), 'dateFormat') ||
c.dateFormat;
// save format because getData can be slow...
if ($header.length) {
$header.data('dateFormat', format);
}
return ts.convertFormat(str, format) || str;
}
return str;
},
type: 'numeric'
});
// match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk
ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i;
ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i;
ts.addParser({
id: 'time',
is: function(str) {
return ts.regex.timeTest.test(str);
},
format: function(str) {
// isolate time... ignore month, day and year
var temp,
timePart = (str || '').match(ts.regex.timeMatch),
orig = new Date(str),
// no time component? default to 00:00 by leaving it out, but only if str is defined
time = str && (timePart !== null ? timePart[0] : '00:00 AM'),
date = time ? new Date('2000/01/01 ' + time.replace(ts.regex.dateReplace, '$1 $2')) : time;
if (date instanceof Date && isFinite(date)) {
temp = orig instanceof Date && isFinite(orig) ? orig.getTime() : 0;
// if original string was a valid date, add it to the decimal so the column sorts in some kind of order
// luckily new Date() ignores the decimals
return temp ? parseFloat(date.getTime() + '.' + orig.getTime()) : date.getTime();
}
return str;
},
type: 'numeric'
});
ts.addParser({
id: 'metadata',
is: function() {
return false;
},
format: function(str, table, cell) {
var c = table.config,
p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
return $(cell).metadata()[p];
},
type: 'numeric'
});
/*
██████ ██████ █████▄ █████▄ ▄████▄
▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██
▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██
██████ ██████ █████▀ ██ ██ ██ ██
*/
// add default widgets
ts.addWidget({
id: 'zebra',
priority: 90,
format: function(table, c, wo) {
var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len,
child = new RegExp(c.cssChildRow, 'i'),
$tbodies = c.$tbodies.add($(c.namespace + '_extra_table').children('tbody:not(.' + c.cssInfoBlock + ')'));
for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++) {
// loop through the visible rows
count = 0;
$visibleRows = $tbodies.eq(tbodyIndex).children('tr:visible').not(c.selectorRemove);
len = $visibleRows.length;
for (rowIndex = 0; rowIndex < len; rowIndex++) {
$row = $visibleRows.eq(rowIndex);
// style child rows the same way the parent row was styled
if (!child.test($row[0].className)) {
count++;
}
isEven = (count % 2 === 0);
$row
.removeClass(wo.zebra[isEven ? 1 : 0])
.addClass(wo.zebra[isEven ? 0 : 1]);
}
}
},
remove: function(table, c, wo, refreshing) {
if (refreshing) {
return;
}
var tbodyIndex, $tbody,
$tbodies = c.$tbodies,
toRemove = (wo.zebra || ['even', 'odd']).join(' ');
for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++) {
$tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
$tbody.children().removeClass(toRemove);
ts.processTbody(table, $tbody, false); // restore tbody
}
}
});
})(jQuery);
return jQuery.tablesorter;
}));