From 32720c56a6e88315a7cf4a2481ef1fd74e391f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 27 Nov 2017 18:24:22 +0100 Subject: [PATCH 01/25] kfet -- Tables are sortable Many tables in kfet app templates become sortable: account list, negative account list, article list, article inventory list, article supplier list, article category list, checkout list, checkout statement list, inventory list, inventory details, order list, order creation, order details. This is achieved thanks to the jQuery plugin 'tablesorter': https://mottie.github.io/tablesorter/docs/ - Affected tables also got sticky headers (it stays visible on scroll). - Dates format are modified in order to ease the date sorting with the plugin (it avoids writing a custom parser, or an extractor from additional hidden element in the table cells). - Tables whose content is classified by category (of articles) now uses several tbodies. This has minor effects on the tables style. - Tags of the header help signs become 'i', instead of 'span', in order to avoid weird spacing. --- kfet/static/kfet/css/base/misc.css | 4 + kfet/static/kfet/css/index.css | 11 + .../kfet/css/libs/jquery-tablesorter-kfet.css | 0 kfet/static/kfet/js/kfet.js | 74 + .../jquery.tablesorter.combined.js | 6014 +++++++++++++++++ kfet/templates/kfet/account.html | 7 +- kfet/templates/kfet/account_negative.html | 17 +- kfet/templates/kfet/article.html | 62 +- .../kfet/article_inventories_snippet.html | 11 +- .../kfet/article_suppliers_snippet.html | 11 +- kfet/templates/kfet/base.html | 1 + kfet/templates/kfet/category.html | 7 +- kfet/templates/kfet/checkout.html | 17 +- kfet/templates/kfet/checkout_read.html | 11 +- kfet/templates/kfet/inventory.html | 11 +- kfet/templates/kfet/inventory_read.html | 44 +- kfet/templates/kfet/order.html | 13 +- kfet/templates/kfet/order_create.html | 105 +- kfet/templates/kfet/order_read.html | 58 +- kfet/views.py | 2 +- 20 files changed, 6331 insertions(+), 149 deletions(-) create mode 100644 kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css create mode 100644 kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js diff --git a/kfet/static/kfet/css/base/misc.css b/kfet/static/kfet/css/base/misc.css index 680fb1ee..fab1a33a 100644 --- a/kfet/static/kfet/css/base/misc.css +++ b/kfet/static/kfet/css/base/misc.css @@ -75,6 +75,10 @@ ul { padding:8px !important; } +.table thead .sm-padding { + padding:3px !important; +} + .table tr.section { background: #c63b52 !important; color:#fff; diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 8e28cce0..fdb86aff 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -3,6 +3,7 @@ /* Libs customizations */ @import url("libs/jconfirm-kfet.css"); +@import url("libs/jquery-tablesorter-kfet.css"); @import url("libs/multiple-select-kfet.css"); /* Base */ @@ -54,6 +55,11 @@ color: #C81022; } +.table thead .glyphicon { + font-size: 12px; + opacity: 0.8; +} + /* * Pages tableaux seuls @@ -82,6 +88,11 @@ border-radius: 0; } +.table td.small-width { + /* Header still extends the width of the column, but it will be minimal. */ + width: 30px; +} + .auth-form { padding: 15px 0; background: #d86c7e; diff --git a/kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css b/kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css new file mode 100644 index 00000000..e69de29b diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 75b80b04..144582ca 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -235,3 +235,77 @@ function submit_url(el) { let url = $(el).data('url'); create_form(url).appendTo($('body')).submit(); } + + +/** + * jquery-tablesorter + * https://mottie.github.io/tablesorter/docs/ + * + * Known bugs (v2.29.0): + * - Sort order icons in sticky headers are not updated. + * Status: Fixed in next release. + * + * TODO: + * - Handle i18n. + */ + + +function registerBoolParser(id, true_str, false_str) { + $.tablesorter.addParser({ + id: id, + format: function(s) { + return s.toLowerCase() + .replace(true_str, 1) + .replace(false_str, 0); + }, + type: 'numeric' + }); +} + + +// Parsers for the text representations of boolean. +registerBoolParser('yesno', 'oui', 'non'); + +registerBoolParser('article__is_sold', 'en vente', 'non vendu'); +registerBoolParser('article__hidden', 'caché', 'affiché'); + + +// https://mottie.github.io/tablesorter/docs/index.html#variable-defaults +$.extend(true, $.tablesorter.defaults, { + headerTemplate: '{content} {icon}', + + cssIconAsc : 'glyphicon glyphicon-chevron-up', + cssIconDesc : 'glyphicon glyphicon-chevron-down', + cssIconNone : 'glyphicon glyphicon-resize-vertical', + + // Only four-digits format year is handled by the builtin parser + // 'shortDate'. + dateFormat: 'ddmmyyyy', + + // Accented characters are replaced with their non-accented one. + sortLocaleCompare: true, + // French format: 1 234,56 + usNumberFormat: false, + + widgets: ['stickyHeaders'], + widgetOptions: { + stickyHeaders_offset: '.navbar', + } +}); + + +// https://mottie.github.io/tablesorter/docs/index.html#variable-language +$.extend($.tablesorter.language, { + sortAsc : 'Trié par ordre croissant, ', + sortDesc : 'Trié par ordre décroissant, ', + sortNone : 'Non trié, ', + sortDisabled : 'tri désactivé et/ou non-modifiable', + nextAsc : 'cliquer pour trier par ordre croissant', + nextDesc : 'cliquer pour trier par ordre décroissant', + nextNone : 'cliquer pour retirer le tri' +}); + + +$( function() { + $('.sortable').tablesorter(); +}); diff --git a/kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js b/kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js new file mode 100644 index 00000000..457b76fb --- /dev/null +++ b/kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js @@ -0,0 +1,6014 @@ +/*! tablesorter (FORK) - updated 09-27-2017 (v2.29.0)*/ +/* Includes widgets ( storage,uitheme,columns,filter,stickyHeaders,resizable,saveSort ) */ +(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.29.0 *//* +* 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.29.0', + + 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} = // 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 ( c.debug ) { + 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 ( c.debug ) { + 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; + 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 ) { + 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 ( c.debug ) { + console.log( 'Overall initialization time:' + ts.benchmark( $.data( table, 'startoveralltimer' ) ) ); + if ( c.debug && 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; + // 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 || + // elements within a button + $target.parents( 'button' ).length > 0 ) { + return !c.cancelSelection; + } + if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { + ts.buildCache( c ); + } + // jQuery v1.2.6 doesn't have closest() + $cell = ts.getHeaderCell( $( this ) ); + // reference original table headers and find the same cell + // don't use $headers or IE8 throws an error - see #987 + temp = $headers.index( $cell ); + c.last.clickedIndex = ( temp < 0 ) ? $cell.attr( 'data-column' ) : temp; + // use column index if $headers is undefined + cell = c.$headers[ c.last.clickedIndex ]; + 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 ( c.debug ) { + 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 ? + '' : + ''; + // 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, $th, + $elem = $( elem ); + // ignore cell (don't add it to c.$headers) if row has ignoreRow class + if ( $elem.parent().hasClass( c.cssIgnoreRow ) ) { return; } + // 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( '
' + template + '
' ); // faster than wrapInner + } + if ( c.onRenderHeader ) { + c.onRenderHeader.apply( $elem, [ index, c, c.$table ] ); + } + // data-column stored on th or td only + $th = ts.getHeaderCell( $elem ); + column = parseInt( $th.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 + }; + 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 ] = {}; + } + $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 ( c.debug ) { + 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 = {}; + // 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 c.debug ? console.warn( 'Warning: *Empty table!* Not building a parser cache' ) : ''; + } else if ( c.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 ( c.debug ) { + debug[ '(' + 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 ( c.debug ) { + if ( !ts.isEmptyObject( debug ) ) { + console[ console.table ? 'table' : 'log' ]( debug ); + } 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 */ + 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 = '', + 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 ( c.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; + // 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 c.debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : ''; + } + if ( c.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 && c.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 ( c.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 ( c.debug ) { + 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' ], + // 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 + c.$headers[ i ].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 ) { + $sorted + .eq( column ) + .removeClass( none ) + .addClass( css[ list[ indx ][ 1 ] ] ) + .attr( 'aria-sort', aria[ list[ indx ][ 1 ] ] ) + .find( '.' + ts.css.icon ) + .removeClass( cssIcon[ 2 ] ) + .addClass( cssIcon[ list[ indx ][ 1 ] ] ); + } + } + // add sorted class to footer & extra headers, if they exist + if ( $extras.length ) { + $extras + .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' ) + .removeClass( none ) + .addClass( css[ list[ indx ][ 1 ] ] ); + } + } + } + } + // add verbose aria labels + len = c.$headers.length; + for ( indx = 0; indx < len; indx++ ) { + ts.setColumnAriaLabel( c, c.$headers.eq( indx ) ); + } + }, + + // This function does NOT return closest if the $el matches the selector + getClosest : function( $el, selector ) { + return $.fn.closest ? + $el.closest( selector ) : + $el.parents( selector ).filter( ':first' ); + }, + + getHeaderCell : function( $el ) { + // jQuery v1.2.6 doesn't have closest() + if ( $.fn.closest ) { + return $el.closest( 'th, td' ); + } + return /TH|TD/.test( $el[0].nodeName ) ? + $el : + $el.parents( 'th, td' ).filter( ':first' ); + }, + + // nextSort (optional), lets you disable next sort text + setColumnAriaLabel : function( c, $header, nextSort ) { + if ( $header.length ) { + var $th = ts.getHeaderCell( $header ), + // data-column always stored on the th/td + column = parseInt( $th.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 ); + } + }, + + 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 ( c.debug ) { + 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 && / 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, + wo = c.widgetOptions, + $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 ( c.debug ) { + 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 || !wo.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 ( c.debug ) { + 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, + // get current column index; *always* stored on th/td + $th = ts.getHeaderCell( $( cell ) ), + col = parseInt( $th.attr( 'data-column' ), 10 ), + order = c.sortVars[ col ].order; + // 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 ) { + // 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 ]; + } + } + } + // add column to sort list + dir = order[ c.sortVars[ col ].count ]; + if ( dir < 2 ) { + c.sortList[ c.sortList.length ] = [ col, dir ]; + // add other columns if header spans across multiple + if ( cell.colSpan > 1 ) { + for ( indx = 1; indx < cell.colSpan; indx++ ) { + c.sortList[ c.sortList.length ] = [ col + indx, dir ]; + // update count on columns in colSpan + c.sortVars[ col + indx ].count = $.inArray( dir, order ); + } + } + } + // 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 + 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 ]; + if ( dir < 2 ) { + c.sortList[ c.sortList.length ] = [ col, dir ]; + // add other columns if header spans across multiple + if ( cell.colSpan > 1 ) { + for ( indx = 1; indx < cell.colSpan; indx++ ) { + c.sortList[ c.sortList.length ] = [ col + indx, dir ]; + // update count on columns in colSpan + c.sortVars[ col + indx ].count = $.inArray( dir, order ); + } + } + } + } + } + // 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 ]; + } + } + } + } + // 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 ( c.debug ) { 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 ( c.debug ) { + 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 table = c.table; + c.$table.triggerHandler( 'sortStart', table ); + // 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 = []; + ts.setHeadersCss( c ); + ts.multisort( c ); + ts.appendCache( c ); + var indx; + for (indx = 0; indx < c.columns; indx++) { + c.sortVars[ indx ].count = -1; + } + 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, + 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 ( c.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 ( c.debug ) { + console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' ); + } + widget.init( table, widget, c, wo ); + } + } + if ( !init && typeof widget.format === 'function' ) { + applied = true; + if ( c.debug ) { + console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' ); + } + widget.format( table, c, wo, false ); + } + if ( c.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, + widgets = []; + // prevent numerous consecutive widget applications + if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) { + return; + } + if ( c.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 ( c.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 ( c.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 ( c.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 ( c.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 ( c.debug ) { + 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 ); + }, + + // $.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 = $( '' ); + 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( $( '' ).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.filter( '[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( '' ); + 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 ( c.debug ) { + 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 ( c.debug ) { + 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, + debug = c.debug, + $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 ( debug ) { + 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, table ) { + 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, table ) { + 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, table ) { + // 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 ); + +/*! Widget: storage - updated 4/18/2017 (v2.28.8) */ +/*global JSON:false */ +;(function ($, window, document) { + 'use strict'; + + var ts = $.tablesorter || {}; + + // update defaults for validator; these values must be falsy! + $.extend(true, ts.defaults, { + fixedUrl: '', + widgetOptions: { + storage_fixedUrl: '', + storage_group: '', + storage_page: '', + storage_storageType: '', + storage_tableId: '', + storage_useSessionStorage: '' + } + }); + + // *** Store data in local storage, with a cookie fallback *** + /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) + if you need it, then include https://github.com/douglascrockford/JSON-js + + $.parseJSON is not available is jQuery versions older than 1.4.1, using older + versions will only allow storing information for one page at a time + + // *** Save data (JSON format only) *** + // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid + var val = { "mywidget" : "data1" }; // valid JSON uses double quotes + // $.tablesorter.storage(table, key, val); + $.tablesorter.storage(table, 'tablesorter-mywidget', val); + + // *** Get data: $.tablesorter.storage(table, key); *** + v = $.tablesorter.storage(table, 'tablesorter-mywidget'); + // val may be empty, so also check for your data + val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; + alert(val); // 'data1' if saved, or '' if not + */ + ts.storage = function(table, key, value, options) { + table = $(table)[0]; + var cookieIndex, cookies, date, + hasStorage = false, + values = {}, + c = table.config, + wo = c && c.widgetOptions, + storageType = ( + ( options && options.storageType ) || ( wo && wo.storage_storageType ) + ).toString().charAt(0).toLowerCase(), + // deprecating "useSessionStorage"; any storageType setting overrides it + session = storageType ? '' : + ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ), + $table = $(table), + // id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId, + // (4) table ID, then (5) table index + id = options && options.id || + $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') || + wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ), + // url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl, + // (4) table.config.fixedUrl (deprecated), then (5) window location path + url = options && options.url || + $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') || + wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname; + + // skip if using cookies + if (storageType !== 'c') { + storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage'; + // https://gist.github.com/paulirish/5558557 + if (storageType in window) { + try { + window[storageType].setItem('_tmptest', 'temp'); + hasStorage = true; + window[storageType].removeItem('_tmptest'); + } catch (error) { + if (c && c.debug) { + console.warn( storageType + ' is not supported in this browser' ); + } + } + } + } + if (c.debug) { + console.log('Storage widget using', hasStorage ? storageType : 'cookies'); + } + // *** get value *** + if ($.parseJSON) { + if (hasStorage) { + values = $.parseJSON( window[storageType][key] || 'null' ) || {}; + } else { + // old browser, using cookies + cookies = document.cookie.split(/[;\s|=]/); + // add one to get from the key to the value + cookieIndex = $.inArray(key, cookies) + 1; + values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {}; + } + } + // allow value to be an empty string too + if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) { + // add unique identifiers = url pathname > table ID/index on page > data + if (!values[url]) { + values[url] = {}; + } + values[url][id] = value; + // *** set value *** + if (hasStorage) { + window[storageType][key] = JSON.stringify(values); + } else { + date = new Date(); + date.setTime(date.getTime() + (31536e+6)); // 365 days + document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; + } + } else { + return values && values[url] ? values[url][id] : ''; + } + }; + +})(jQuery, window, document); + +/*! Widget: uitheme - updated 9/27/2017 (v2.29.0) */ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + ts.themes = { + 'bootstrap' : { + table : 'table table-bordered table-striped', + caption : 'caption', + // header class names + header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css) + sortNone : '', + sortAsc : '', + sortDesc : '', + active : '', // applied when column is sorted + hover : '', // custom css required - a defined bootstrap style may not override other classes + // icon class names + icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the in the header + iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted + iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort + iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort + filterRow : '', // filter row class + footerRow : '', + footerCells : '', + even : '', // even row zebra striping + odd : '' // odd row zebra striping + }, + 'jui' : { + table : 'ui-widget ui-widget-content ui-corner-all', // table classes + caption : 'ui-widget-content', + // header class names + header : 'ui-widget-header ui-corner-all ui-state-default', // header classes + sortNone : '', + sortAsc : '', + sortDesc : '', + active : 'ui-state-active', // applied when column is sorted + hover : 'ui-state-hover', // hover class + // icon class names + icons : 'ui-icon', // icon class added to the in the header + iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted + iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort + iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort + filterRow : '', + footerRow : '', + footerCells : '', + even : 'ui-widget-content', // even row zebra striping + odd : 'ui-state-default' // odd row zebra striping + } + }; + + $.extend(ts.css, { + wrapper : 'tablesorter-wrapper' // ui theme & resizable + }); + + ts.addWidget({ + id: 'uitheme', + priority: 10, + format: function(table, c, wo) { + var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme, + themesAll = ts.themes, + $table = c.$table.add( $( c.namespace + '_extra_table' ) ), + $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ), + theme = c.theme || 'jui', + themes = themesAll[theme] || {}, + remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ), + iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) ); + if (c.debug) { time = new Date(); } + // initialization code - run once + if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) { + wo.uitheme_applied = true; + oldtheme = themesAll[c.appliedTheme] || {}; + hasOldTheme = !$.isEmptyObject(oldtheme); + oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : ''; + oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : ''; + if (hasOldTheme) { + wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') ); + wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') ); + c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') ); + } + // update zebra stripes + if (themes.even) { wo.zebra[0] += ' ' + themes.even; } + if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; } + // add caption style + $table.children('caption') + .removeClass(oldtheme.caption || '') + .addClass(themes.caption); + // add table/footer class names + $tfoot = $table + // remove other selected themes + .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') ) + .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name + .children('tfoot'); + c.appliedTheme = c.theme; + + if ($tfoot.length) { + $tfoot + // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed + .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow) + .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells); + } + // update header classes + $headers + .removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' ) + .addClass(themes.header) + .not('.sorter-false') + .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') + .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { + // toggleClass with switch added in jQuery 1.3 + $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || ''); + }); + + $headers.each(function(){ + var $this = $(this); + if (!$this.find('.' + ts.css.wrapper).length) { + // Firefox needs this inner div to position the icon & resizer correctly + $this.wrapInner('
'); + } + }); + if (c.cssIcon) { + // if c.cssIcon is '', then no is added to the header + $headers + .find('.' + ts.css.icon) + .removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '') + .addClass(themes.icons || ''); + } + // filter widget initializes after uitheme + if (ts.hasWidget( c.table, 'filter' )) { + tmp = function() { + $table.children('thead').children('.' + ts.css.filterRow) + .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '') + .addClass(themes.filterRow || ''); + }; + if (wo.filter_initialized) { + tmp(); + } else { + $table.one('filterInit', function() { + tmp(); + }); + } + } + } + for (i = 0; i < c.columns; i++) { + $header = c.$headers + .add($(c.namespace + '_extra_headers')) + .not('.sorter-false') + .filter('[data-column="' + i + '"]'); + $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $(); + $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last'); + if ($h.length) { + $header.removeClass(remove); + $icon.removeClass(iconRmv); + if ($h[0].sortDisabled) { + // no sort arrows for disabled columns! + $icon.removeClass(themes.icons || ''); + } else { + hdr = themes.sortNone; + icon = themes.iconSortNone; + if ($h.hasClass(ts.css.sortAsc)) { + hdr = [ themes.sortAsc, themes.active ].join(' '); + icon = themes.iconSortAsc; + } else if ($h.hasClass(ts.css.sortDesc)) { + hdr = [ themes.sortDesc, themes.active ].join(' '); + icon = themes.iconSortDesc; + } + $header.addClass(hdr); + $icon.addClass(icon || ''); + } + } + } + if (c.debug) { + console.log('Applying ' + theme + ' theme' + ts.benchmark(time)); + } + }, + remove: function(table, c, wo, refreshing) { + if (!wo.uitheme_applied) { return; } + var $table = c.$table, + theme = c.appliedTheme || 'jui', + themes = ts.themes[ theme ] || ts.themes.jui, + $headers = $table.children('thead').children(), + remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc, + iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc; + $table.removeClass('tablesorter-' + theme + ' ' + themes.table); + wo.uitheme_applied = false; + if (refreshing) { return; } + $table.find(ts.css.header).removeClass(themes.header); + $headers + .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover + .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) + .filter('.' + ts.css.filterRow) + .removeClass(themes.filterRow); + $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv); + } + }); + +})(jQuery); + +/*! Widget: columns - updated 5/24/2017 (v2.28.11) */ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + ts.addWidget({ + id: 'columns', + priority: 65, + options : { + columns : [ 'primary', 'secondary', 'tertiary' ] + }, + format: function(table, c, wo) { + var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, + $table = c.$table, + $tbodies = c.$tbodies, + sortList = c.sortList, + len = sortList.length, + // removed c.widgetColumns support + css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ], + last = css.length - 1; + remove = css.join(' '); + // check if there is a sort (on initialization there may not be one) + for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody + $rows = $tbody.children('tr'); + // loop through the visible rows + $rows.each(function() { + $row = $(this); + if (this.style.display !== 'none') { + // remove all columns class names + $cells = $row.children().removeClass(remove); + // add appropriate column class names + if (sortList && sortList[0]) { + // primary sort column class + $cells.eq(sortList[0][0]).addClass(css[0]); + if (len > 1) { + for (indx = 1; indx < len; indx++) { + // secondary, tertiary, etc sort column classes + $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); + } + } + } + } + }); + ts.processTbody(table, $tbody, false); + } + // add classes to thead and tfoot + rows = wo.columns_thead !== false ? [ 'thead tr' ] : []; + if (wo.columns_tfoot !== false) { + rows.push('tfoot tr'); + } + if (rows.length) { + $rows = $table.find( rows.join(',') ).children().removeClass(remove); + if (len) { + for (indx = 0; indx < len; indx++) { + // add primary. secondary, tertiary, etc sort column classes + $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); + } + } + } + }, + remove: function(table, c, wo) { + var tbodyIndex, $tbody, + $tbodies = c.$tbodies, + remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' '); + c.$headers.removeClass(remove); + c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); + for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody + $tbody.children('tr').each(function() { + $(this).children().removeClass(remove); + }); + ts.processTbody(table, $tbody, false); // restore tbody + } + } + }); + +})(jQuery); + +/*! Widget: filter - updated 7/4/2017 (v2.28.15) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +;( function ( $ ) { + 'use strict'; + var tsf, tsfRegex, + ts = $.tablesorter || {}, + tscss = ts.css, + tskeyCodes = ts.keyCodes; + + $.extend( tscss, { + filterRow : 'tablesorter-filter-row', + filter : 'tablesorter-filter', + filterDisabled : 'disabled', + filterRowHide : 'hideme' + }); + + $.extend( tskeyCodes, { + backSpace : 8, + escape : 27, + space : 32, + left : 37, + down : 40 + }); + + ts.addWidget({ + id: 'filter', + priority: 50, + options : { + filter_cellFilter : '', // css class name added to the filter cell ( string or array ) + filter_childRows : false, // if true, filter includes child row content in the search + filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped + filter_childWithSibs : true, // if true, include matching child row siblings + filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query ) + filter_columnFilters : true, // if true, a filter will be added to the top of each table column + filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added ) + filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value + filter_defaultFilter : {}, // add a default column filter type '~{query}' to make fuzzy searches default; '{q1} AND {q2}' to make all searches use a logical AND. + filter_excludeFilter : {}, // filters to exclude, per column + filter_external : '', // jQuery selector string ( or jQuery object ) of external filters + filter_filteredRow : 'filtered', // class added to filtered rows; define in css with "display:none" to hide the filtered-out rows + filter_formatter : null, // add custom filter elements to the filter row + filter_functions : null, // add custom filter functions using this option + filter_hideEmpty : true, // hide filter row when table is empty + filter_hideFilters : false, // collapse filter row when mouse leaves the area + filter_ignoreCase : true, // if true, make all searches case-insensitive + filter_liveSearch : true, // if true, search column content while the user types ( with a delay ) + filter_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class + filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available ( visible ) options within the drop down + filter_placeholder : { search : '', select : '' }, // default placeholder text ( overridden by any header 'data-placeholder' setting ) + filter_reset : null, // jQuery selector string of an element used to reset the filters + filter_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers + filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters + filter_searchDelay : 300, // typing delay in milliseconds before starting a search + filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true + filter_selectSource : null, // include a function to return an array of values to be added to the column filter select + filter_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text + filter_serversideFiltering : false, // if true, must perform server-side filtering b/c client-side filtering is disabled, but the ui and events will still be used. + filter_startsWith : false, // if true, filter start from the beginning of the cell contents + filter_useParsedData : false // filter all data using parsed content + }, + format: function( table, c, wo ) { + if ( !c.$table.hasClass( 'hasFilters' ) ) { + tsf.init( table, c, wo ); + } + }, + remove: function( table, c, wo, refreshing ) { + var tbodyIndex, $tbody, + $table = c.$table, + $tbodies = c.$tbodies, + events = ( + 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + + 'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit ' + ).split( ' ' ).join( c.namespace + 'filter ' ); + $table + .removeClass( 'hasFilters' ) + // add filter namespace to all BUT search + .unbind( events.replace( ts.regex.spaces, ' ' ) ) + // remove the filter row even if refreshing, because the column might have been moved + .find( '.' + tscss.filterRow ).remove(); + wo.filter_initialized = false; + if ( refreshing ) { return; } + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody + $tbody.children().removeClass( wo.filter_filteredRow ).show(); + ts.processTbody( table, $tbody, false ); // restore tbody + } + if ( wo.filter_reset ) { + $( document ).undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ); + } + } + }); + + tsf = ts.filter = { + + // regex used in filter 'check' functions - not for general use and not documented + regex: { + regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // regex to test for regex + child : /tablesorter-childRow/, // child row class name; this gets updated in the script + filtered : /filtered/, // filtered (hidden) row class name; updated in the script + type : /undefined|number/, // check type + exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==') + operators : /[<>=]/g, // replace operators + query : '(q|query)', // replace filter queries + wild01 : /\?/g, // wild card match 0 or 1 + wild0More : /\*/g, // wild care match 0 or more + quote : /\"/g, + isNeg1 : /(>=?\s*-\d)/, + isNeg2 : /(<=?\s*\d)/ + }, + // function( c, data ) { } + // c = table.config + // data.$row = jQuery object of the row currently being processed + // data.$cells = jQuery object of all cells within the current row + // data.filters = array of filters for all columns ( some may be undefined ) + // data.filter = filter for the current column + // data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true ) + // data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string ) + // data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string ) + // data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true ) + // data.cacheArray = An array of parsed content from each table cell in the row being processed + // data.index = column index; table = table element ( DOM ) + // data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class ) + types: { + or : function( c, data, vars ) { + // look for "|", but not if it is inside of a regular expression + if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) && + // this test for regex has potential to slow down the overall search + !tsfRegex.regex.test( data.filter ) ) { + var indx, filterMatched, query, regex, + // duplicate data but split filter + data2 = $.extend( {}, data ), + filter = data.filter.split( tsfRegex.orSplit ), + iFilter = data.iFilter.split( tsfRegex.orSplit ), + len = filter.length; + for ( indx = 0; indx < len; indx++ ) { + data2.nestedFilters = true; + data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); + data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); + query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')'; + try { + // use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search, + // e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group + regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); + // filterMatched = data2.filter === '' && indx > 0 ? true + // look for an exact match with the 'or' unless the 'filter-match' class is found + filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ); + if ( filterMatched ) { + return filterMatched; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; + } + return null; + }, + // Look for an AND or && operator ( logical and ) + and : function( c, data, vars ) { + if ( tsfRegex.andTest.test( data.filter ) ) { + var indx, filterMatched, result, query, regex, + // duplicate data but split filter + data2 = $.extend( {}, data ), + filter = data.filter.split( tsfRegex.andSplit ), + iFilter = data.iFilter.split( tsfRegex.andSplit ), + len = filter.length; + for ( indx = 0; indx < len; indx++ ) { + data2.nestedFilters = true; + data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); + data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); + query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' ) + // replace wild cards since /(a*)/i will match anything + .replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ); + try { + // use try/catch just in case RegExp is invalid + regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); + // look for an exact match with the 'and' unless the 'filter-match' class is found + result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) ); + if ( indx === 0 ) { + filterMatched = result; + } else { + filterMatched = filterMatched && result; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; + } + return null; + }, + // Look for regex + regex: function( c, data ) { + if ( tsfRegex.regex.test( data.filter ) ) { + var matches, + // cache regex per column for optimal speed + regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ), + isRegex = regex instanceof RegExp; + try { + if ( !isRegex ) { + // force case insensitive search if ignoreCase option set? + // if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; } + data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] ); + } + matches = regex.test( data.exact ); + } catch ( error ) { + matches = false; + } + return matches; + } + return null; + }, + // Look for operators >, >=, < or <= + operators: function( c, data ) { + // ignore empty strings... because '' < 10 is true + if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) { + var cachedValue, result, txt, + table = c.table, + parsed = data.parsed[ data.index ], + query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ), + parser = c.parsers[ data.index ] || {}, + savedSearch = query; + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || parser.type === 'numeric' ) { + txt = $.trim( '' + data.iFilter.replace( tsfRegex.operators, '' ) ); + result = tsf.parseFilter( c, txt, data, true ); + query = ( typeof result === 'number' && result !== '' && !isNaN( result ) ) ? result : query; + } + // iExact may be numeric - see issue #149; + // check if cached is defined, because sometimes j goes out of range? ( numeric columns ) + if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) && + typeof data.cache !== 'undefined' ) { + cachedValue = data.cache; + } else { + txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + cachedValue = ts.formatFloat( txt, table ); + } + if ( tsfRegex.gtTest.test( data.iFilter ) ) { + result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query; + } else if ( tsfRegex.ltTest.test( data.iFilter ) ) { + result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query; + } + // keep showing all rows if nothing follows the operator + if ( !result && savedSearch === '' ) { + result = true; + } + return result; + } + return null; + }, + // Look for a not match + notMatch: function( c, data ) { + if ( tsfRegex.notTest.test( data.iFilter ) ) { + var indx, + txt = data.iFilter.replace( '!', '' ), + filter = tsf.parseFilter( c, txt, data ) || ''; + if ( tsfRegex.exact.test( filter ) ) { + // look for exact not matches - see #628 + filter = filter.replace( tsfRegex.exact, '' ); + return filter === '' ? true : $.trim( filter ) !== data.iExact; + } else { + indx = data.iExact.search( $.trim( filter ) ); + return filter === '' ? true : + // return true if not found + data.anyMatch ? indx < 0 : + // return false if found + !( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0 ); + } + } + return null; + }, + // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric + exact: function( c, data ) { + /*jshint eqeqeq:false */ + if ( tsfRegex.exact.test( data.iFilter ) ) { + var txt = data.iFilter.replace( tsfRegex.exact, '' ), + filter = tsf.parseFilter( c, txt, data ) || ''; + return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact; + } + return null; + }, + // Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu! + range : function( c, data ) { + if ( tsfRegex.toTest.test( data.iFilter ) ) { + var result, tmp, range1, range2, + table = c.table, + index = data.index, + parsed = data.parsed[index], + // make sure the dash is for a range and not indicating a negative number + query = data.iFilter.split( tsfRegex.toSplit ); + + tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; + range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; + range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || c.parsers[ index ].type === 'numeric' ) { + result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); + range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; + result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); + range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; + } + if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { + result = data.cache; + } else { + tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + result = ts.formatFloat( tmp, table ); + } + if ( range1 > range2 ) { + tmp = range1; range1 = range2; range2 = tmp; // swap + } + return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); + } + return null; + }, + // Look for wild card: ? = single, * = multiple, or | = logical OR + wild : function( c, data ) { + if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { + var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); + // look for an exact match with the 'or' unless the 'filter-match' class is found + if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { + query = data.isMatch ? query : '^(' + query + ')$'; + } + // parsing the filter may not work properly when using wildcards =/ + try { + return new RegExp( + query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), + c.widgetOptions.filter_ignoreCase ? 'i' : '' + ) + .test( data.exact ); + } catch ( error ) { + return null; + } + } + return null; + }, + // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) + fuzzy: function( c, data ) { + if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { + var indx, + patternIndx = 0, + len = data.iExact.length, + txt = data.iFilter.slice( 1 ), + pattern = tsf.parseFilter( c, txt, data ) || ''; + for ( indx = 0; indx < len; indx++ ) { + if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { + patternIndx += 1; + } + } + return patternIndx === pattern.length; + } + return null; + } + }, + init: function( table ) { + // filter language options + ts.language = $.extend( true, {}, { + to : 'to', + or : 'or', + and : 'and' + }, ts.language ); + + var options, string, txt, $header, column, val, fxn, noSelect, + c = table.config, + wo = c.widgetOptions; + c.$table.addClass( 'hasFilters' ); + c.lastSearch = []; + + // define timers so using clearTimeout won't cause an undefined error + wo.filter_searchTimer = null; + wo.filter_initTimer = null; + wo.filter_formatterCount = 0; + wo.filter_formatterInit = []; + wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]'; + wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]'; + + val = '\\{' + tsfRegex.query + '\\}'; + $.extend( tsfRegex, { + child : new RegExp( c.cssChildRow ), + filtered : new RegExp( wo.filter_filteredRow ), + alreadyFiltered : new RegExp( '(\\s+(' + ts.language.or + '|-|' + ts.language.to + ')\\s+)', 'i' ), + toTest : new RegExp( '\\s+(-|' + ts.language.to + ')\\s+', 'i' ), + toSplit : new RegExp( '(?:\\s+(?:-|' + ts.language.to + ')\\s+)', 'gi' ), + andTest : new RegExp( '\\s+(' + ts.language.and + '|&&)\\s+', 'i' ), + andSplit : new RegExp( '(?:\\s+(?:' + ts.language.and + '|&&)\\s+)', 'gi' ), + orTest : new RegExp( '(\\||\\s+' + ts.language.or + '\\s+)', 'i' ), + orSplit : new RegExp( '(?:\\s+(?:' + ts.language.or + ')\\s+|\\|)', 'gi' ), + iQuery : new RegExp( val, 'i' ), + igQuery : new RegExp( val, 'ig' ), + operTest : /^[<>]=?/, + gtTest : />/, + gteTest : />=/, + ltTest : /' + + ( $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || + '' + ) + + '' : ''; + val = string; + txt = string; + if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + val = string.split( wo.filter_selectSourceSeparator ); + txt = val[1]; + val = val[0]; + } + options += ''; + } + } + c.$table + .find( 'thead' ) + .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) + .append( options ); + txt = wo.filter_selectSource; + fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column ); + if ( fxn ) { + // updating so the extra options are appended + tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) ); + } + } + } + } + } + // not really updating, but if the column has both the 'filter-select' class & + // filter_functions set to true, it would append the same options twice. + tsf.buildDefault( table, true ); + + tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); + if ( wo.filter_external ) { + tsf.bindSearch( table, wo.filter_external ); + } + + if ( wo.filter_hideFilters ) { + tsf.hideFilters( c ); + } + + // show processing icon + if ( c.showProcessing ) { + txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table + .unbind( txt.replace( ts.regex.spaces, ' ' ) ) + .bind( txt, function( event, columns ) { + // only add processing to certain columns to all columns + $header = ( columns ) ? + c.$table + .find( '.' + tscss.header ) + .filter( '[data-column]' ) + .filter( function() { + return columns[ $( this ).data( 'column' ) ] !== ''; + }) : ''; + ts.isProcessing( table, event.type === 'filterStart', columns ? $header : '' ); + }); + } + + // set filtered rows count ( intially unfiltered ) + c.filteredRows = c.totalRows; + + // add default values + txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table + .unbind( txt.replace( ts.regex.spaces, ' ' ) ) + .bind( txt, function() { + tsf.completeInit( this ); + }); + // if filter widget is added after pager has initialized; then set filter init flag + if ( c.pager && c.pager.initialized && !wo.filter_initialized ) { + c.$table.triggerHandler( 'filterFomatterUpdate' ); + setTimeout( function() { + tsf.filterInitComplete( c ); + }, 100 ); + } else if ( !wo.filter_initialized ) { + tsf.completeInit( table ); + } + }, + completeInit: function( table ) { + // redefine 'c' & 'wo' so they update properly inside this callback + var c = table.config, + wo = c.widgetOptions, + filters = tsf.setDefaults( table, c, wo ) || []; + if ( filters.length ) { + // prevent delayInit from triggering a cache build if filters are empty + if ( !( c.delayInit && filters.join( '' ) === '' ) ) { + ts.setFilters( table, filters, true ); + } + } + c.$table.triggerHandler( 'filterFomatterUpdate' ); + // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers + setTimeout( function() { + if ( !wo.filter_initialized ) { + tsf.filterInitComplete( c ); + } + }, 100 ); + }, + + // $cell parameter, but not the config, is passed to the filter_formatters, + // so we have to work with it instead + formatterUpdated: function( $cell, column ) { + // prevent error if $cell is undefined - see #1056 + var $table = $cell && $cell.closest( 'table' ); + var config = $table.length && $table[0].config, + wo = config && config.widgetOptions; + if ( wo && !wo.filter_initialized ) { + // add updates by column since this function + // may be called numerous times before initialization + wo.filter_formatterInit[ column ] = 1; + } + }, + filterInitComplete: function( c ) { + var indx, len, + wo = c.widgetOptions, + count = 0, + completed = function() { + wo.filter_initialized = true; + // update lastSearch - it gets cleared often + c.lastSearch = c.$table.data( 'lastSearch' ); + c.$table.triggerHandler( 'filterInit', c ); + tsf.findRows( c.table, c.lastSearch || [] ); + }; + if ( $.isEmptyObject( wo.filter_formatter ) ) { + completed(); + } else { + len = wo.filter_formatterInit.length; + for ( indx = 0; indx < len; indx++ ) { + if ( wo.filter_formatterInit[ indx ] === 1 ) { + count++; + } + } + clearTimeout( wo.filter_initTimer ); + if ( !wo.filter_initialized && count === wo.filter_formatterCount ) { + // filter widget initialized + completed(); + } else if ( !wo.filter_initialized ) { + // fall back in case a filter_formatter doesn't call + // $.tablesorter.filter.formatterUpdated( $cell, column ), and the count is off + wo.filter_initTimer = setTimeout( function() { + completed(); + }, 500 ); + } + } + }, + // encode or decode filters for storage; see #1026 + processFilters: function( filters, encode ) { + var indx, + // fixes #1237; previously returning an encoded "filters" value + result = [], + mode = encode ? encodeURIComponent : decodeURIComponent, + len = filters.length; + for ( indx = 0; indx < len; indx++ ) { + if ( filters[ indx ] ) { + result[ indx ] = mode( filters[ indx ] ); + } + } + return result; + }, + setDefaults: function( table, c, wo ) { + var isArray, saved, indx, col, $filters, + // get current ( default ) filters + filters = ts.getFilters( table ) || []; + if ( wo.filter_saveFilters && ts.storage ) { + saved = ts.storage( table, 'tablesorter-filters' ) || []; + isArray = $.isArray( saved ); + // make sure we're not just getting an empty array + if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) { + filters = tsf.processFilters( saved ); + } + } + // if no filters saved, then check default settings + if ( filters.join( '' ) === '' ) { + // allow adding default setting to external filters + $filters = c.$headers.add( wo.filter_$externalFilters ) + .filter( '[' + wo.filter_defaultAttrib + ']' ); + for ( indx = 0; indx <= c.columns; indx++ ) { + // include data-column='all' external filters + col = indx === c.columns ? 'all' : indx; + filters[ indx ] = $filters + .filter( '[data-column="' + col + '"]' ) + .attr( wo.filter_defaultAttrib ) || filters[indx] || ''; + } + } + c.$table.data( 'lastSearch', filters ); + return filters; + }, + parseFilter: function( c, filter, data, parsed ) { + return parsed || data.parsed[ data.index ] ? + c.parsers[ data.index ].format( filter, c.table, [], data.index ) : + filter; + }, + buildRow: function( table, c, wo ) { + var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp, + // c.columns defined in computeThIndexes() + cellFilter = wo.filter_cellFilter, + columns = c.columns, + arry = $.isArray( cellFilter ), + buildFilter = ''; + for ( column = 0; column < columns; column++ ) { + if ( c.$headerIndexed[ column ].length ) { + // account for entire column set with colspan. See #1047 + tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0; + if ( tmp > 1 ) { + buildFilter += '' ).appendTo( $filter ); + } else { + ffxn = ts.getColumnData( table, wo.filter_formatter, column ); + if ( ffxn ) { + wo.filter_formatterCount++; + buildFilter = ffxn( $filter, column ); + // no element returned, so lets go find it + if ( buildFilter && buildFilter.length === 0 ) { + buildFilter = $filter.children( 'input' ); + } + // element not in DOM, so lets attach it + if ( buildFilter && ( buildFilter.parent().length === 0 || + ( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) { + $filter.append( buildFilter ); + } + } else { + buildFilter = $( '' ).appendTo( $filter ); + } + if ( buildFilter ) { + tmp = $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.search || ''; + buildFilter.attr( 'placeholder', tmp ); + } + } + if ( buildFilter ) { + // add filter class name + name = ( $.isArray( wo.filter_cssFilter ) ? + ( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) : + wo.filter_cssFilter ) || ''; + // copy data-column from table cell (it will include colspan) + buildFilter.addClass( tscss.filter + ' ' + name ).attr( 'data-column', $filter.attr( 'data-column' ) ); + if ( disabled ) { + buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true; + } + } + } + } + }, + bindSearch: function( table, $el, internal ) { + table = $( table )[0]; + $el = $( $el ); // allow passing a selector string + if ( !$el.length ) { return; } + var tmp, + c = table.config, + wo = c.widgetOptions, + namespace = c.namespace + 'filter', + $ext = wo.filter_$externalFilters; + if ( internal !== true ) { + // save anyMatch element + tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector; + wo.filter_$anyMatch = $el.filter( tmp ); + if ( $ext && $ext.length ) { + wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); + } else { + wo.filter_$externalFilters = $el; + } + // update values ( external filters added after table initialization ) + ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false ); + } + // unbind events + tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) ); + $el + // use data attribute instead of jQuery data since the head is cloned without including + // the data/binding + .attr( 'data-lastSearchTime', new Date().getTime() ) + .unbind( tmp.replace( ts.regex.spaces, ' ' ) ) + .bind( 'keydown' + namespace, function( event ) { + if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) { + // prevent keypress event + return false; + } + }) + .bind( 'keyup' + namespace, function( event ) { + wo = table.config.widgetOptions; // make sure "wo" isn't cached + var column = parseInt( $( this ).attr( 'data-column' ), 10 ), + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch : + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( typeof liveSearch === 'undefined' ) { + liveSearch = wo.filter_liveSearch.fallback || false; + } + $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); + // emulate what webkit does.... escape clears the filter + if ( event.which === tskeyCodes.escape ) { + // make sure to restore the last value on escape + this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column]; + // don't return if the search value is empty ( all rows need to be revealed ) + } else if ( this.value !== '' && ( + // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace + ( typeof liveSearch === 'number' && this.value.length < liveSearch ) || + // let return & backspace continue on, but ignore arrows & non-valid characters + ( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace && + ( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) { + return; + // live search + } else if ( liveSearch === false ) { + if ( this.value !== '' && event.which !== tskeyCodes.enter ) { + return; + } + } + // change event = no delay; last true flag tells getFilters to skip newest timed input + tsf.searching( table, true, true, column ); + }) + // include change for select - fixes #473 + .bind( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) { + // don't get cached data, in case data-column changes dynamically + var column = parseInt( $( this ).attr( 'data-column' ), 10 ), + eventType = event.type, + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? + wo.filter_liveSearch : + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( table.config.widgetOptions.filter_initialized && + // immediate search if user presses enter + ( event.which === tskeyCodes.enter || + // immediate search if a "search" or "blur" is triggered on the input + ( eventType === 'search' || eventType === 'blur' ) || + // change & input events must be ignored if liveSearch !== true + ( eventType === 'change' || eventType === 'input' ) && + // prevent search if liveSearch is a number + ( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) && + // don't allow 'change' or 'input' event to process if the input value + // is the same - fixes #685 + this.value !== c.lastSearch[column] + ) + ) { + event.preventDefault(); + // init search with no delay + $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); + tsf.searching( table, eventType !== 'keypress', true, column ); + } + }); + }, + searching: function( table, filter, skipFirst, column ) { + var liveSearch, + wo = table.config.widgetOptions; + if (typeof column === 'undefined') { + // no delay + liveSearch = false; + } else { + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? + wo.filter_liveSearch : + // get column setting, or set to fallback value, or default to false + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( typeof liveSearch === 'undefined' ) { + liveSearch = wo.filter_liveSearch.fallback || false; + } + } + clearTimeout( wo.filter_searchTimer ); + if ( typeof filter === 'undefined' || filter === true ) { + // delay filtering + wo.filter_searchTimer = setTimeout( function() { + tsf.checkFilters( table, filter, skipFirst ); + }, liveSearch ? wo.filter_searchDelay : 10 ); + } else { + // skip delay + tsf.checkFilters( table, filter, skipFirst ); + } + }, + equalFilters: function (c, filter1, filter2) { + var indx, + f1 = [], + f2 = [], + len = c.columns + 1; // add one to include anyMatch filter + filter1 = $.isArray(filter1) ? filter1 : []; + filter2 = $.isArray(filter2) ? filter2 : []; + for (indx = 0; indx < len; indx++) { + f1[indx] = filter1[indx] || ''; + f2[indx] = filter2[indx] || ''; + } + return f1.join(',') === f2.join(','); + }, + checkFilters: function( table, filter, skipFirst ) { + var c = table.config, + wo = c.widgetOptions, + filterArray = $.isArray( filter ), + filters = ( filterArray ) ? filter : ts.getFilters( table, true ), + currentFilters = filters || []; // current filter values + // prevent errors if delay init is set + if ( $.isEmptyObject( c.cache ) ) { + // update cache if delayInit set & pager has initialized ( after user initiates a search ) + if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) { + ts.updateCache( c, function() { + tsf.checkFilters( table, false, skipFirst ); + }); + } + return; + } + // add filter array back into inputs + if ( filterArray ) { + ts.setFilters( table, filters, false, skipFirst !== true ); + if ( !wo.filter_initialized ) { + c.lastSearch = []; + c.lastCombinedFilter = ''; + } + } + if ( wo.filter_hideFilters ) { + // show/hide filter row as needed + c.$table + .find( '.' + tscss.filterRow ) + .triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' ); + } + // return if the last search is the same; but filter === false when updating the search + // see example-widget-filter.html filter toggle buttons + if ( tsf.equalFilters(c, c.lastSearch, currentFilters) && filter !== false ) { + return; + } else if ( filter === false ) { + // force filter refresh + c.lastCombinedFilter = ''; + c.lastSearch = []; + } + // define filter inside it is false + filters = filters || []; + // convert filters to strings - see #1070 + filters = Array.prototype.map ? + filters.map( String ) : + // for IE8 & older browsers - maybe not the best method + filters.join( '\ufffd' ).split( '\ufffd' ); + + if ( wo.filter_initialized ) { + c.$table.triggerHandler( 'filterStart', [ filters ] ); + } + if ( c.showProcessing ) { + // give it time for the processing icon to kick in + setTimeout( function() { + tsf.findRows( table, filters, currentFilters ); + return false; + }, 30 ); + } else { + tsf.findRows( table, filters, currentFilters ); + return false; + } + }, + hideFiltersCheck: function( c ) { + if (typeof c.widgetOptions.filter_hideFilters === 'function') { + var val = c.widgetOptions.filter_hideFilters( c ); + if (typeof val === 'boolean') { + return val; + } + } + return ts.getFilters( c.$table ).join( '' ) === ''; + }, + hideFilters: function( c, $table ) { + var timer; + ( $table || c.$table ) + .find( '.' + tscss.filterRow ) + .addClass( tscss.filterRowHide ) + .bind( 'mouseenter mouseleave', function( e ) { + // save event object - http://bugs.jquery.com/ticket/12140 + var event = e, + $row = $( this ); + clearTimeout( timer ); + timer = setTimeout( function() { + if ( /enter|over/.test( event.type ) ) { + $row.removeClass( tscss.filterRowHide ); + } else { + // don't hide if input has focus + // $( ':focus' ) needs jQuery 1.6+ + if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) { + // don't hide row if any filter has a value + $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) ); + } + } + }, 200 ); + }) + .find( 'input, select' ).bind( 'focus blur', function( e ) { + var event = e, + $row = $( this ).closest( 'tr' ); + clearTimeout( timer ); + timer = setTimeout( function() { + clearTimeout( timer ); + // don't hide row if any filter has a value + $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' ); + }, 200 ); + }); + }, + defaultFilter: function( filter, mask ) { + if ( filter === '' ) { return filter; } + var regex = tsfRegex.iQuery, + maskLen = mask.match( tsfRegex.igQuery ).length, + query = maskLen > 1 ? $.trim( filter ).split( /\s/ ) : [ $.trim( filter ) ], + len = query.length - 1, + indx = 0, + val = mask; + if ( len < 1 && maskLen > 1 ) { + // only one 'word' in query but mask has >1 slots + query[1] = query[0]; + } + // replace all {query} with query words... + // if query = 'Bob', then convert mask from '!{query}' to '!Bob' + // if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank' + while ( regex.test( val ) ) { + val = val.replace( regex, query[indx++] || '' ); + if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) { + val = mask.replace( regex, val ); + } + } + return val; + }, + getLatestSearch: function( $input ) { + if ( $input ) { + return $input.sort( function( a, b ) { + return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' ); + }); + } + return $input || $(); + }, + findRange: function( c, val, ignoreRanges ) { + // look for multiple columns '1-3,4-6,8' in data-column + var temp, ranges, range, start, end, singles, i, indx, len, + columns = []; + if ( /^[0-9]+$/.test( val ) ) { + // always return an array + return [ parseInt( val, 10 ) ]; + } + // process column range + if ( !ignoreRanges && /-/.test( val ) ) { + ranges = val.match( /(\d+)\s*-\s*(\d+)/g ); + len = ranges ? ranges.length : 0; + for ( indx = 0; indx < len; indx++ ) { + range = ranges[indx].split( /\s*-\s*/ ); + start = parseInt( range[0], 10 ) || 0; + end = parseInt( range[1], 10 ) || ( c.columns - 1 ); + if ( start > end ) { + temp = start; start = end; end = temp; // swap + } + if ( end >= c.columns ) { + end = c.columns - 1; + } + for ( ; start <= end; start++ ) { + columns[ columns.length ] = start; + } + // remove processed range from val + val = val.replace( ranges[ indx ], '' ); + } + } + // process single columns + if ( !ignoreRanges && /,/.test( val ) ) { + singles = val.split( /\s*,\s*/ ); + len = singles.length; + for ( i = 0; i < len; i++ ) { + if ( singles[ i ] !== '' ) { + indx = parseInt( singles[ i ], 10 ); + if ( indx < c.columns ) { + columns[ columns.length ] = indx; + } + } + } + } + // return all columns + if ( !columns.length ) { + for ( indx = 0; indx < c.columns; indx++ ) { + columns[ columns.length ] = indx; + } + } + return columns; + }, + getColumnElm: function( c, $elements, column ) { + // data-column may contain multiple columns '1-3,5-6,8' + // replaces: c.$filters.filter( '[data-column="' + column + '"]' ); + return $elements.filter( function() { + var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) ); + return $.inArray( column, cols ) > -1; + }); + }, + multipleColumns: function( c, $input ) { + // look for multiple columns '1-3,4-6,8' in data-column + var wo = c.widgetOptions, + // only target 'all' column inputs on initialization + // & don't target 'all' column inputs if they don't exist + targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length, + val = $.trim( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ); + return tsf.findRange( c, val, !targets ); + }, + processTypes: function( c, data, vars ) { + var ffxn, + filterMatched = null, + matches = null; + for ( ffxn in tsf.types ) { + if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) { + matches = tsf.types[ffxn]( c, data, vars ); + if ( matches !== null ) { + filterMatched = matches; + } + } + } + return filterMatched; + }, + matchType: function( c, columnIndex ) { + var isMatch, + wo = c.widgetOptions, + $el = c.$headerIndexed[ columnIndex ]; + // filter-exact > filter-match > filter_matchType for type + if ( $el.hasClass( 'filter-exact' ) ) { + isMatch = false; + } else if ( $el.hasClass( 'filter-match' ) ) { + isMatch = true; + } else { + // filter-select is not applied when filter_functions are used, so look for a select + if ( wo.filter_columnFilters ) { + $el = c.$filters + .find( '.' + tscss.filter ) + .add( wo.filter_$externalFilters ) + .filter( '[data-column="' + columnIndex + '"]' ); + } else if ( wo.filter_$externalFilters ) { + $el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' ); + } + isMatch = $el.length ? + c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' : + // default to exact, if no inputs found + false; + } + return isMatch; + }, + processRow: function( c, data, vars ) { + var result, filterMatched, + fxn, ffxn, txt, + wo = c.widgetOptions, + showRow = true, + hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length, + + // if wo.filter_$anyMatch data-column attribute is changed dynamically + // we don't want to do an "anyMatch" search on one column using data + // for the entire row - see #998 + columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ? + // look for multiple columns '1-3,4-6,8' + tsf.multipleColumns( c, wo.filter_$anyMatch ) : + []; + data.$cells = data.$row.children(); + if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { + data.anyMatch = true; + data.isMatch = true; + data.rowArray = data.$cells.map( function( i ) { + if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { + if ( data.parsed[ i ] ) { + txt = data.cacheArray[ i ]; + } else { + txt = data.rawArray[ i ]; + txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt ); + if ( c.sortLocaleCompare ) { + txt = ts.replaceAccents( txt ); + } + } + return txt; + } + }).get(); + data.filter = data.anyMatchFilter; + data.iFilter = data.iAnyMatchFilter; + data.exact = data.rowArray.join( ' ' ); + data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact; + data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' ); + vars.excludeMatch = vars.noAnyMatch; + filterMatched = tsf.processTypes( c, data, vars ); + if ( filterMatched !== null ) { + showRow = filterMatched; + } else { + if ( wo.filter_startsWith ) { + showRow = false; + // data.rowArray may not contain all columns + columnIndex = Math.min( c.columns, data.rowArray.length ); + while ( !showRow && columnIndex > 0 ) { + columnIndex--; + showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0; + } + } else { + showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0; + } + } + data.anyMatch = false; + // no other filters to process + if ( data.filters.join( '' ) === data.filter ) { + return showRow; + } + } + + for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { + data.filter = data.filters[ columnIndex ]; + data.index = columnIndex; + + // filter types to exclude, per column + vars.excludeMatch = vars.excludeFilter[ columnIndex ]; + + // ignore if filter is empty or disabled + if ( data.filter ) { + data.cache = data.cacheArray[ columnIndex ]; + result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || ''; + data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405 + data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ? + data.exact.toLowerCase() : data.exact; + data.isMatch = tsf.matchType( c, columnIndex ); + + result = showRow; // if showRow is true, show that row + + // in case select filter option has a different value vs text 'a - z|A through Z' + ffxn = wo.filter_columnFilters ? + c.$filters.add( wo.filter_$externalFilters ) + .filter( '[data-column="' + columnIndex + '"]' ) + .find( 'select option:selected' ) + .attr( 'data-function-name' ) || '' : ''; + // replace accents - see #357 + if ( c.sortLocaleCompare ) { + data.filter = ts.replaceAccents( data.filter ); + } + + // replace column specific default filters - see #1088 + if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) { + data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] ); + } + + // data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ), + // data.filter = case sensitive + data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter; + fxn = vars.functions[ columnIndex ]; + filterMatched = null; + if ( fxn ) { + if ( typeof fxn === 'function' ) { + // filter callback( exact cell content, parser normalized content, + // filter input value, column index, jQuery row object ) + filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); + } else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) { + // selector option function + txt = ffxn || data.filter; + filterMatched = + fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); + } + } + if ( filterMatched === null ) { + // cycle through the different filters + // filters return a boolean or null if nothing matches + filterMatched = tsf.processTypes( c, data, vars ); + if ( filterMatched !== null ) { + result = filterMatched; + // Look for match, and add child row data for matching + } else { + // check fxn (filter-select in header) after filter types are checked + // without this, the filter + jQuery UI selectmenu demo was breaking + if ( fxn === true ) { + // default selector uses exact match unless 'filter-match' class is found + result = data.isMatch ? + // data.iExact may be a number + ( '' + data.iExact ).search( data.iFilter ) >= 0 : + data.filter === data.exact; + } else { + txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) ); + result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) ); + } + } + } else { + result = filterMatched; + } + showRow = ( result ) ? showRow : false; + } + } + return showRow; + }, + findRows: function( table, filters, currentFilters ) { + if ( + tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) || + !table.config.widgetOptions.filter_initialized + ) { + return; + } + var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex, + isChild, childRow, lastSearch, showRow, showParent, time, val, indx, + notFiltered, searchFiltered, query, injected, res, id, txt, + storedFilters = $.extend( [], filters ), + c = table.config, + wo = c.widgetOptions, + // data object passed to filters; anyMatch is a flag for the filters + data = { + anyMatch: false, + filters: filters, + // regex filter type cache + filter_regexCache : [] + }, + vars = { + // anyMatch really screws up with these types of filters + noAnyMatch: [ 'range', 'operators' ], + // cache filter variables that use ts.getColumnData in the main loop + functions : [], + excludeFilter : [], + defaultColFilter : [], + defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '' + }; + + // parse columns after formatter, in case the class is added at that point + data.parsed = []; + for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { + data.parsed[ columnIndex ] = wo.filter_useParsedData || + // parser has a "parsed" parameter + ( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed || + // getData may not return 'parsed' if other 'filter-' class names exist + // ( e.g. ) + ts.getData && ts.getData( c.$headerIndexed[ columnIndex ], + ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' || + c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) ); + + vars.functions[ columnIndex ] = + ts.getColumnData( table, wo.filter_functions, columnIndex ) || + c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' ); + vars.defaultColFilter[ columnIndex ] = + ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || ''; + vars.excludeFilter[ columnIndex ] = + ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ ); + } + + if ( c.debug ) { + console.log( 'Filter: Starting filter widget search', filters ); + time = new Date(); + } + // filtered rows count + c.filteredRows = 0; + c.totalRows = 0; + currentFilters = ( storedFilters || [] ); + + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true ); + // skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel! + // $rows = $tbody.children( 'tr' ).not( c.selectorRemove ); + columnIndex = c.columns; + // convert stored rows into a jQuery object + norm_rows = c.cache[ tbodyIndex ].normalized; + $rows = $( $.map( norm_rows, function( el ) { + return el[ columnIndex ].$row.get(); + }) ); + + if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) { + $rows + .removeClass( wo.filter_filteredRow ) + .not( '.' + c.cssChildRow ) + .css( 'display', '' ); + } else { + // filter out child rows + $rows = $rows.not( '.' + c.cssChildRow ); + len = $rows.length; + + if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) || + typeof filters[c.columns] !== 'undefined' ) { + data.anyMatchFlag = true; + data.anyMatchFilter = '' + ( + filters[ c.columns ] || + wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() || + '' + ); + if ( wo.filter_columnAnyMatch ) { + // specific columns search + query = data.anyMatchFilter.split( tsfRegex.andSplit ); + injected = false; + for ( indx = 0; indx < query.length; indx++ ) { + res = query[ indx ].split( ':' ); + if ( res.length > 1 ) { + // make the column a one-based index ( non-developers start counting from one :P ) + if ( isNaN( res[0] ) ) { + $.each( c.headerContent, function( i, txt ) { + // multiple matches are possible + if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) { + id = i; + filters[ id ] = res[1]; + } + }); + } else { + id = parseInt( res[0], 10 ) - 1; + } + if ( id >= 0 && id < c.columns ) { // if id is an integer + filters[ id ] = res[1]; + query.splice( indx, 1 ); + indx--; + injected = true; + } + } + } + if ( injected ) { + data.anyMatchFilter = query.join( ' && ' ); + } + } + } + + // optimize searching only through already filtered rows - see #313 + searchFiltered = wo.filter_searchFiltered; + lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || []; + if ( searchFiltered ) { + // cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669 + for ( indx = 0; indx < columnIndex + 1; indx++ ) { + val = filters[indx] || ''; + // break out of loop if we've already determined not to search filtered rows + if ( !searchFiltered ) { indx = columnIndex; } + // search already filtered rows if... + searchFiltered = searchFiltered && lastSearch.length && + // there are no changes from beginning of filter + val.indexOf( lastSearch[indx] || '' ) === 0 && + // if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string + !tsfRegex.alreadyFiltered.test( val ) && + // if we are not doing exact matches, using '|' ( logical or ) or not '!' + !tsfRegex.exactTest.test( val ) && + // don't search only filtered if the value is negative + // ( '> -10' => '> -100' will ignore hidden rows ) + !( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) && + // if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593 + !( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length && + !tsf.matchType( c, indx ) ); + } + } + notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length; + // can't search when all rows are hidden - this happens when looking for exact matches + if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; } + if ( c.debug ) { + console.log( 'Filter: Searching through ' + + ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' ); + } + if ( data.anyMatchFlag ) { + if ( c.sortLocaleCompare ) { + // replace accents + data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter ); + } + if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) { + data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter ); + // clear search filtered flag because default filters are not saved to the last search + searchFiltered = false; + } + // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true + // when c.ignoreCase is true, the cache contains all lower case data + data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ? + data.anyMatchFilter : + data.anyMatchFilter.toLowerCase(); + } + + // loop through the rows + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + + txt = $rows[ rowIndex ].className; + // the first row can never be a child row + isChild = rowIndex && tsfRegex.child.test( txt ); + // skip child rows & already filtered rows + if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) { + continue; + } + + data.$row = $rows.eq( rowIndex ); + data.rowIndex = rowIndex; + data.cacheArray = norm_rows[ rowIndex ]; + rowData = data.cacheArray[ c.columns ]; + data.rawArray = rowData.raw; + data.childRowText = ''; + + if ( !wo.filter_childByColumn ) { + txt = ''; + // child row cached text + childRow = rowData.child; + // so, if 'table.config.widgetOptions.filter_childRows' is true and there is + // a match anywhere in the child row, then it will make the row visible + // checked here so the option can be changed dynamically + for ( indx = 0; indx < childRow.length; indx++ ) { + txt += ' ' + childRow[indx].join( ' ' ) || ''; + } + data.childRowText = wo.filter_childRows ? + ( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) : + ''; + } + + showRow = false; + showParent = tsf.processRow( c, data, vars ); + $row = rowData.$row; + + // don't pass reference to val + val = showParent ? true : false; + childRow = rowData.$row.filter( ':gt(0)' ); + if ( wo.filter_childRows && childRow.length ) { + if ( wo.filter_childByColumn ) { + if ( !wo.filter_childWithSibs ) { + // hide all child rows + childRow.addClass( wo.filter_filteredRow ); + // if only showing resulting child row, only include parent + $row = $row.eq( 0 ); + } + // cycle through each child row + for ( indx = 0; indx < childRow.length; indx++ ) { + data.$row = childRow.eq( indx ); + data.cacheArray = rowData.child[ indx ]; + data.rawArray = data.cacheArray; + val = tsf.processRow( c, data, vars ); + // use OR comparison on child rows + showRow = showRow || val; + if ( !wo.filter_childWithSibs && val ) { + childRow.eq( indx ).removeClass( wo.filter_filteredRow ); + } + } + } + // keep parent row match even if no child matches... see #1020 + showRow = showRow || showParent; + } else { + showRow = val; + } + $row + .toggleClass( wo.filter_filteredRow, !showRow )[0] + .display = showRow ? '' : 'none'; + } + } + c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length; + c.totalRows += $rows.length; + ts.processTbody( table, $tbody, false ); + } + // lastCombinedFilter is no longer used internally + c.lastCombinedFilter = storedFilters.join(''); // save last search + // don't save 'filters' directly since it may have altered ( AnyMatch column searches ) + c.lastSearch = storedFilters; + c.$table.data( 'lastSearch', storedFilters ); + if ( wo.filter_saveFilters && ts.storage ) { + ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) ); + } + if ( c.debug ) { + console.log( 'Completed filter widget search' + ts.benchmark(time) ); + } + if ( wo.filter_initialized ) { + c.$table.triggerHandler( 'filterBeforeEnd', c ); + c.$table.triggerHandler( 'filterEnd', c ); + } + setTimeout( function() { + ts.applyWidget( c.table ); // make sure zebra widget is applied + }, 0 ); + }, + getOptionSource: function( table, column, onlyAvail ) { + table = $( table )[0]; + var c = table.config, + wo = c.widgetOptions, + arry = false, + source = wo.filter_selectSource, + last = c.$table.data( 'lastSearch' ) || [], + fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column ); + + if ( onlyAvail && last[column] !== '' ) { + onlyAvail = false; + } + + // filter select source option + if ( fxn === true ) { + // OVERALL source + arry = source( table, column, onlyAvail ); + } else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '' ) >= 0 ) ) { + // selectSource is a jQuery object or string of options + return fxn; + } else if ( $.isArray( fxn ) ) { + arry = fxn; + } else if ( $.type( source ) === 'object' && fxn ) { + // custom select source function for a SPECIFIC COLUMN + arry = fxn( table, column, onlyAvail ); + // abort - updating the selects from an external method + if (arry === null) { + return null; + } + } + if ( arry === false ) { + // fall back to original method + arry = tsf.getOptions( table, column, onlyAvail ); + } + + return tsf.processOptions( table, column, arry ); + + }, + processOptions: function( table, column, arry ) { + if ( !$.isArray( arry ) ) { + return false; + } + table = $( table )[0]; + var cts, txt, indx, len, parsedTxt, str, + c = table.config, + validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns, + direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false, + parsed = []; + // get unique elements and sort the list + // if $.tablesorter.sortText exists ( not in the original tablesorter ), + // then natural sort the list otherwise use a basic sort + arry = $.grep( arry, function( value, indx ) { + if ( value.text ) { + return true; + } + return $.inArray( value, arry ) === indx; + }); + if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) { + // unsorted select options + return arry; + } else { + len = arry.length; + // parse select option values + for ( indx = 0; indx < len; indx++ ) { + txt = arry[ indx ]; + // check for object + str = txt.text ? txt.text : txt; + // sortNatural breaks if you don't pass it strings + parsedTxt = ( validColumn && c.parsers && c.parsers.length && + c.parsers[ column ].format( str, table, [], column ) || str ).toString(); + parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt; + // parse array data using set column parser; this DOES NOT pass the original + // table cell to the parser format function + if ( txt.text ) { + txt.parsed = parsedTxt; + parsed[ parsed.length ] = txt; + } else { + parsed[ parsed.length ] = { + text : txt, + // check parser length - fixes #934 + parsed : parsedTxt + }; + } + } + // sort parsed select options + cts = c.textSorter || ''; + parsed.sort( function( a, b ) { + var x = direction ? b.parsed : a.parsed, + y = direction ? a.parsed : b.parsed; + if ( validColumn && typeof cts === 'function' ) { + // custom OVERALL text sorter + return cts( x, y, true, column, table ); + } else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) { + // custom text sorter for a SPECIFIC COLUMN + return cts[column]( x, y, true, column, table ); + } else if ( ts.sortNatural ) { + // fall back to natural sort + return ts.sortNatural( x, y ); + } + // using an older version! do a basic sort + return true; + }); + // rebuild arry from sorted parsed data + arry = []; + len = parsed.length; + for ( indx = 0; indx < len; indx++ ) { + arry[ arry.length ] = parsed[indx]; + } + return arry; + } + }, + getOptions: function( table, column, onlyAvail ) { + table = $( table )[0]; + var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen, + c = table.config, + wo = c.widgetOptions, + arry = []; + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + cache = c.cache[tbodyIndex]; + len = c.cache[tbodyIndex].normalized.length; + // loop through the rows + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + // get cached row from cache.row ( old ) or row data object + // ( new; last item in normalized array ) + row = cache.row ? + cache.row[ rowIndex ] : + cache.normalized[ rowIndex ][ c.columns ].$row[0]; + // check if has class filtered + if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) { + continue; + } + // get non-normalized cell content + if ( wo.filter_useParsedData || + c.parsers[column].parsed || + c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) { + arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ]; + // child row parsed data + if ( wo.filter_childRows && wo.filter_childByColumn ) { + childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1; + for ( indx = 0; indx < childLen; indx++ ) { + arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ]; + } + } + } else { + // get raw cached data instead of content directly from the cells + arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ]; + // child row unparsed data + if ( wo.filter_childRows && wo.filter_childByColumn ) { + childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length; + for ( indx = 1; indx < childLen; indx++ ) { + child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column ); + arry[ arry.length ] = '' + ts.getElementText( c, child, column ); + } + } + } + } + } + return arry; + }, + buildSelect: function( table, column, arry, updating, onlyAvail ) { + table = $( table )[0]; + column = parseInt( column, 10 ); + if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) { + return; + } + + var indx, val, txt, t, $filters, $filter, option, + c = table.config, + wo = c.widgetOptions, + node = c.$headerIndexed[ column ], + // t.data( 'placeholder' ) won't work in jQuery older than 1.4.3 + options = '', + // Get curent filter value + currentValue = c.$table + .find( 'thead' ) + .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) + .val(); + + // nothing included in arry ( external source ), so get the options from + // filter_selectSource or column data + if ( typeof arry === 'undefined' || arry === '' ) { + arry = tsf.getOptionSource( table, column, onlyAvail ); + // abort, selects are updated by an external method + if (arry === null) { + return; + } + } + + if ( $.isArray( arry ) ) { + // build option list + for ( indx = 0; indx < arry.length; indx++ ) { + option = arry[ indx ]; + if ( option.text ) { + // OBJECT!! add data-function-name in case the value is set in filter_functions + option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value; + + // support jQuery < v1.8, otherwise the below code could be shortened to + // options += $( '