/*! Widget: math - updated 5/3/2017 (v2.28.9) *//* * Requires tablesorter v2.16+ and jQuery 1.7+ * by Rob Garrison */ /*jshint browser:true, jquery:true, unused:false */ /*global jQuery: false */ ;( function( $ ) { 'use strict'; var ts = $.tablesorter, math = { error: { 0 : 'Infinity result: Divide by zero', 1 : 'Need more than one element to make this calculation', 'undef' : 'No elements found' }, // value returned when calculation is not possible, e.g. no values, dividing by zero, etc. invalid : function( c, name, errorIndex ) { // name = function returning invalid results // errorIndex = math.error index with an explanation of the error console.warn( name, math.error[ errorIndex ] ); return c && c.widgetOptions.math_none || ''; // text for cell }, events : ( 'tablesorter-initialized update updateAll updateRows addRows updateCell filterReset ' ) .split(' ').join('.tsmath '), processText : function( c, $cell ) { var tmp, txt = ts.getElementText( c, $cell, math.getCellIndex( $cell ) ), prefix = c.widgetOptions.math_prefix; if ( /' + prefix + '' ).text() .replace(/\{content\}/g, '').trim(); txt = txt.replace( tmp, '' ); } txt = ts.formatFloat( txt.replace( /[^\w,. \-()]/g, '' ), c.table ) || 0; // isNaN('') => false return isNaN( txt ) ? 0 : txt; }, // get all of the row numerical values in an arry getRow : function( c, $el, hasFilter ) { var $cells, wo = c.widgetOptions, arry = [], $row = $el.closest( 'tr' ), isFiltered = $row.hasClass( wo.filter_filteredRow || 'filtered' ); if ( hasFilter ) { $row = $row.filter( hasFilter ); } if ( hasFilter || !isFiltered ) { $cells = $row.children().not( '[' + wo.math_dataAttrib + '=ignore]' ); if ( wo.math_ignore.length ) { $cells = $cells.filter( function() { // using $.inArray is not optimal (needed for IE8) return $.inArray( math.getCellIndex( $( this ) ), wo.math_ignore ) === -1; }); } arry = $cells.not( $el ).map( function() { return math.processText( c, $( this ) ); }).get(); } return arry; }, // get all of the column numerical values in an arry getColumn : function( c, $el, type, hasFilter ) { var index, $t, $tr, len, $mathRows, mathAbove, wo = c.widgetOptions, arry = [], $row = $el.closest( 'tr' ), mathAttr = wo.math_dataAttrib, mathIgnore = '[' + mathAttr + '=ignore]', filtered = wo.filter_filteredRow || 'filtered', cIndex = math.getCellIndex( $el ), // get all rows to keep row indexing $rows = c.$table.children( 'tbody' ).children(), mathAttrs = [ '[' + mathAttr + '^=above]', '[' + mathAttr + '^=below]', '[' + mathAttr + '^=col]', '[' + mathAttr + '^=all]' ]; if ( type === 'above' ) { len = $rows.index( $row ); index = len; while ( index >= 0 ) { $tr = $rows.eq( index ); mathAbove = $tr.children().filter( mathAttrs[0] ).length; if ( hasFilter ) { $tr = $tr.filter( hasFilter ); } $t = $tr.children().filter( function() { return math.getCellIndex( $( this ) ) === cIndex; }); // ignore filtered rows & rows with data-math="ignore" (and starting row) if ( ( ( hasFilter || !$tr.hasClass( filtered ) ) && $tr.not( mathIgnore ).length && index !== len ) || mathAbove && index !== len ) { // stop calculating 'above', when encountering another 'above' if ( mathAbove ) { index = 0; } else if ( $t.length ) { arry[ arry.length ] = math.processText( c, $t ); } } index--; } } else if ( type === 'below' ) { len = $rows.length; // index + 1 to ignore starting node for ( index = $rows.index( $row ) + 1; index < len; index++ ) { $tr = $rows.eq( index ); if ( $tr.children().filter( mathAttrs[1] ).length ) { break; } if ( hasFilter ) { $tr = $tr.filter( hasFilter ); } $t = $tr.children().filter( function() { return math.getCellIndex( $( this ) ) === cIndex; }); if ( ( hasFilter || !$tr.hasClass( filtered ) ) && $tr.not( mathIgnore ).length && $t.length ) { arry[ arry.length ] = math.processText( c, $t ); } } } else { $mathRows = $rows.not( mathIgnore ); len = $mathRows.length; for ( index = 0; index < len; index++ ) { $tr = $mathRows.eq( index ); if ( hasFilter ) { $tr = $tr.filter( hasFilter ); } $t = $tr.children().filter( function() { return math.getCellIndex( $( this ) ) === cIndex; }); if ( ( hasFilter || !$tr.hasClass( filtered ) ) && $t.not( mathAttrs.join( ',' ) ).length && !$t.is( $el ) ) { arry[ arry.length ] = math.processText( c, $t ); } } } return arry; }, // get all of the column numerical values in an arry getAll : function( c, hasFilter ) { var $t, col, $row, rowIndex, rowLen, $cells, cellIndex, cellLen, arry = [], wo = c.widgetOptions, mathAttr = wo.math_dataAttrib, mathIgnore = '[' + mathAttr + '=ignore]', filtered = wo.filter_filteredRow || 'filtered', $rows = c.$table.children( 'tbody' ).children().not( mathIgnore ); rowLen = $rows.length; for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) { $row = $rows.eq( rowIndex ); if ( hasFilter ) { $row = $row.filter( hasFilter ); } if ( hasFilter || !$row.hasClass( filtered ) ) { $cells = $row.children().not( mathIgnore ); cellLen = $cells.length; for ( cellIndex = 0; cellIndex < cellLen; cellIndex++ ) { $t = $cells.eq( cellIndex ); col = math.getCellIndex( $t ); if ( !$t.filter( '[' + mathAttr + ']' ).length && $.inArray( col, wo.math_ignore ) < 0 ) { arry[ arry.length ] = math.processText( c, $t ); } } } } return arry; }, setColumnIndexes : function( c ) { var $table = c.$table, last = 1, // only target rows with a colspan or rows included in a rowspan $rows = $table.children( 'tbody' ).children().filter( function() { var cells, indx, $this = $( this ), include = $this.children( '[colspan]' ).length > 0; if ( last > 1 ) { last--; include = true; } else if ( last < 1 ) { last = 1; } if ( $this.children( '[rowspan]' ).length > 0 ) { cells = this.cells; // find max rowspan (in case more than one cell has a rowspan) for ( indx = 0; indx < cells.length; indx++ ) { last = Math.max( cells[ indx ].rowSpan, last ); } } return include; }); // pass `c` (table.config) to computeColumnIndex so it won't add a data-column // to every tbody cell, just the ones where the .cellIndex property doesn't match // the calculated cell index - hopefully fixes the lag issue in #1048 ts.computeColumnIndex( $rows, c ); }, getCellIndex : function( $cell ) { var indx = $cell.attr( 'data-column' ); return typeof indx === 'undefined' ? $cell[0].cellIndex : parseInt( indx, 10 ); }, recalculate : function(c, wo, init) { if ( c && ( !wo.math_isUpdating || init ) ) { var time, mathAttr, $mathCells, indx, len, changed = false, filters = {}; if ( c.debug || wo.math_debug ) { time = new Date(); } // add data-column attributes to all table cells if ( init ) { math.setColumnIndexes( c ) ; } // data-attribute name (defaults to data-math) wo.math_dataAttrib = 'data-' + (wo.math_data || 'math'); // all non-info tbody cells mathAttr = wo.math_dataAttrib; $mathCells = c.$tbodies.children( 'tr' ).children( '[' + mathAttr + ']' ); changed = math.mathType( c, $mathCells, wo.math_priority ) || changed; // only info tbody cells $mathCells = c.$table .children( '.' + c.cssInfoBlock + ', tfoot' ) .children( 'tr' ) .children( '[' + mathAttr + ']' ); math.mathType( c, $mathCells, wo.math_priority ); // find the 'all' total $mathCells = c.$table.children().children( 'tr' ).children( '[' + mathAttr + '^=all]' ); len = $mathCells.length; // get math filter, if any // hasFilter = $row.attr( mathAttr + '-filter' ) || wo.math_rowFilter; for (indx = 0; indx < len; indx++) { var $cell = $mathCells.eq( indx ), filter = $cell.attr( mathAttr + '-filter' ) || wo.math_rowFilter; filters[ filter ] = filters[ filter ] ? filters[ filter ].add( $cell ) : $cell; } $.each( filters, function( hasFilter, $cells ) { changed = math.mathType( c, $cells, [ 'all' ], hasFilter ) || changed; }); // trigger an update only if cells inside the tbody changed if ( changed ) { wo.math_isUpdating = true; if ( c.debug || wo.math_debug ) { console[ console.group ? 'group' : 'log' ]( 'Math widget updating the cache after recalculation' ); } // update internal cache, but ignore "remove-me" rows and do not resort ts.updateCache( c, function() { math.updateComplete( c ); if ( !init && typeof wo.math_completed === 'function' ) { wo.math_completed( c ); } if ( c.debug || wo.math_debug ) { console.log( 'Math widget update completed' + ts.benchmark( time ) ); } }); } else { if ( !init && typeof wo.math_completed === 'function' ) { wo.math_completed( c ); } if ( c.debug || wo.math_debug ) { console.log( 'Math widget found no changes in data' + ts.benchmark( time ) ); } } } }, updateComplete : function( c ) { var wo = c.widgetOptions; if ( wo.math_isUpdating && (c.debug || wo.math_debug ) && console.groupEnd ) { console.groupEnd(); } wo.math_isUpdating = false; }, mathType : function( c, $cells, priority, hasFilter ) { if ( $cells.length ) { var getAll, changed = false, wo = c.widgetOptions, mathAttr = wo.math_dataAttrib, equations = ts.equations; if ( priority[0] === 'all' ) { // mathType is called multiple times if more than one "hasFilter" is used getAll = math.getAll( c, hasFilter ); } if (c.debug || wo.math_debug) { console[ console.group ? 'group' : 'log' ]( 'Tablesorter Math widget recalculation' ); } // $.each is okay here... only 4 priorities $.each( priority, function( i, type ) { var index, arry, formula, result, $el, $targetCells = $cells.filter( '[' + mathAttr + '^=' + type + ']' ), len = $targetCells.length; if ( len ) { if (c.debug || wo.math_debug) { console[ console.group ? 'group' : 'log' ]( type ); } for ( index = 0; index < len; index++ ) { $el = $targetCells.eq( index ); // Row is filtered, no need to do further checking if ( $el.parent().hasClass( wo.filter_filteredRow || 'filtered' ) ) { continue; } hasFilter = $el.attr( mathAttr + '-filter' ) || wo.math_rowFilter; formula = ( $el.attr( mathAttr ) || '' ).replace( type + '-', '' ); arry = ( type === 'row' ) ? math.getRow( c, $el, hasFilter ) : ( type === 'all' ) ? getAll : math.getColumn( c, $el, type, hasFilter ); if ( equations[ formula ] ) { if ( arry.length ) { result = equations[ formula ]( arry, c ); if ( c.debug || wo.math_debug ) { console.log( $el.attr( mathAttr ), hasFilter ? '("' + hasFilter + '")' : '', arry, '=', result ); } } else { // mean will return a divide by zero error, everything else shows an undefined error result = math.invalid( c, formula, formula === 'mean' ? 0 : 'undef' ); } changed = math.output( $el, c, result, arry ) || changed; } } if ( ( c.debug || wo.math_debug ) && console.groupEnd ) { console.groupEnd(); } } }); if ( ( c.debug || wo.math_debug ) && console.groupEnd ) { console.groupEnd(); } return changed; } return false; }, output : function( $cell, c, value, arry ) { // get mask from cell data-attribute: data-math-mask="#,##0.00" var $el, wo = c.widgetOptions, changed = false, prev = $cell.html(), mask = $cell.attr( 'data-' + wo.math_data + '-mask' ) || wo.math_mask, target = $cell.attr( 'data-' + wo.math_data + '-target' ) || '', result = ts.formatMask( mask, value, wo.math_prefix, wo.math_suffix ); if (target) { $el = $cell.find(target); if ($el.length) { $cell = $el; } } if ( typeof wo.math_complete === 'function' ) { result = wo.math_complete( $cell, wo, result, value, arry ); } if ( result !== false ) { changed = prev !== result; $cell.html( result ); } // check if in a regular tbody, otherwise don't pass a changed flag // to prevent unnecessary updating of the table cache if ( changed ) { $el = $cell.closest( 'tbody' ); // content was changed in a tfoot, info-only tbody or the resulting tbody is in a nested table // then don't signal a change if ( !$el.length || $el.hasClass( c.cssInfoBlock ) || $el.parent()[0] !== c.table ) { return false; } } return changed; } }; // Modified from https://code.google.com/p/javascript-number-formatter/ /** * @preserve IntegraXor Web SCADA - JavaScript Number Formatter * http:// www.integraxor.com/ * author: KPL, KHL * (c)2011 ecava * Dual licensed under the MIT or GPL Version 2 licenses. */ ts.formatMask = function( mask, val, tmpPrefix, tmpSuffix ) { if ( !mask || isNaN( +val ) ) { return val; // return as it is. } var isNegative, result, decimal, group, posLeadZero, posTrailZero, posSeparator, part, szSep, integer, str, offset, index, end, inv, suffix = '', // find prefix/suffix len = mask.length, start = mask.search( /[0-9\-\+#]/ ), tmp = start > 0 ? mask.substring( 0, start ) : '', prefix = tmp; if ( tmpPrefix ) { if ( /\{content\}/.test( tmpPrefix || '' ) ) { prefix = ( tmpPrefix || '' ).replace( /\{content\}/g, tmp || '' ); } else { prefix = ( tmpPrefix || '' ) + tmp; } } // reverse string: not an ideal method if there are surrogate pairs inv = mask.split( '' ).reverse().join( '' ); end = inv.search( /[0-9\-\+#]/ ); index = len - end; index += ( mask.substring( index, index + 1 ) === '.' ) ? 1 : 0; tmp = end > 0 ? mask.substring( index, len ) : ''; suffix = tmp; if ( tmpSuffix ) { if ( /\{content\}/.test( tmpSuffix || '' ) ) { suffix = ( tmpSuffix || '' ).replace( /\{content\}/g, tmp || '' ); } else { suffix = tmp + ( tmpSuffix || '' ); } } mask = mask.substring( start, index ); // convert any string to number according to formation sign. val = mask.charAt( 0 ) === '-' ? -val : +val; isNegative = val < 0 ? val = -val : 0; // process only abs(), and turn on flag. // search for separator for grp & decimal, anything not digit, not +/- sign, not #. result = mask.match( /[^\d\-\+#]/g ); decimal = ( result && result[ result.length - 1 ] ) || '.'; // treat the right most symbol as decimal group = ( result && result[ 1 ] && result[ 0 ] ) || ','; // treat the left most symbol as group separator // split the decimal for the format string if any. mask = mask.split( decimal ); // Fix the decimal first, toFixed will auto fill trailing zero. val = val.toFixed( mask[ 1 ] && mask[ 1 ].length ); val = +( val ) + ''; // convert number to string to trim off *all* trailing decimal zero(es) // fill back any trailing zero according to format posTrailZero = mask[ 1 ] && mask[ 1 ].lastIndexOf( '0' ); // look for last zero in format part = val.split( '.' ); // integer will get !part[1] if ( !part[ 1 ] || ( part[ 1 ] && part[ 1 ].length <= posTrailZero ) ) { val = ( +val ).toFixed( posTrailZero + 1 ); } szSep = mask[ 0 ].split( group ); // look for separator mask[ 0 ] = szSep.join( '' ); // join back without separator for counting the pos of any leading 0. posLeadZero = mask[ 0 ] && mask[ 0 ].indexOf( '0' ); if ( posLeadZero > -1 ) { while ( part[ 0 ].length < ( mask[ 0 ].length - posLeadZero ) ) { part[ 0 ] = '0' + part[ 0 ]; } } else if ( +part[ 0 ] === 0 ) { part[ 0 ] = ''; } val = val.split( '.' ); val[ 0 ] = part[ 0 ]; // process the first group separator from decimal (.) only, the rest ignore. // get the length of the last slice of split result. posSeparator = ( szSep[ 1 ] && szSep[ szSep.length - 1 ].length ); if ( posSeparator ) { integer = val[ 0 ]; str = ''; offset = integer.length % posSeparator; len = integer.length; for ( index = 0; index < len; index++ ) { str += integer.charAt( index ); // ie6 only support charAt for sz. // -posSeparator so that won't trail separator on full length /*jshint -W018 */ if ( !( ( index - offset + 1 ) % posSeparator ) && index < len - posSeparator ) { str += group; } } val[ 0 ] = str; } val[ 1 ] = ( mask[ 1 ] && val[ 1 ] ) ? decimal + val[ 1 ] : ''; // put back any negation, combine integer and fraction, and add back prefix & suffix return prefix + ( ( isNegative ? '-' : '' ) + val[ 0 ] + val[ 1 ] ) + suffix; }; ts.equations = { count : function( arry ) { return arry.length; }, sum : function( arry ) { var index, len = arry.length, total = 0; for ( index = 0; index < len; index++ ) { total += arry[ index ]; } return total; }, mean : function( arry ) { var total = ts.equations.sum( arry ); return total / arry.length; }, median : function( arry, c ) { var half, len = arry.length; if ( len > 1 ) { // https://gist.github.com/caseyjustus/1166258 arry.sort( function( a, b ) { return a - b; } ); half = Math.floor( len / 2 ); return ( len % 2 ) ? arry[ half ] : ( arry[ half - 1 ] + arry[ half ] ) / 2; } return math.invalid( c, 'median', 1 ); }, mode : function( arry ) { // http://stackoverflow.com/a/3451640/145346 var index, el, m, modeMap = {}, maxCount = 1, modes = [ arry[ 0 ] ]; for ( index = 0; index < arry.length; index++ ) { el = arry[ index ]; modeMap[ el ] = modeMap[ el ] ? modeMap[ el ] + 1 : 1; m = modeMap[ el ]; if ( m > maxCount ) { modes = [ el ]; maxCount = m; } else if ( m === maxCount ) { modes[ modes.length ] = el; maxCount = m; } } // returns arry of modes if there is a tie return modes.sort( function( a, b ) { return a - b; } ); }, max : function(arry) { return Math.max.apply( Math, arry ); }, min : function(arry) { return Math.min.apply( Math, arry ); }, range: function(arry) { var v = arry.sort( function( a, b ) { return a - b; } ); return v[ arry.length - 1 ] - v[ 0 ]; }, // common variance equation // (not accessible via data-attribute setting) variance: function( arry, population, c ) { var divisor, avg = ts.equations.mean( arry ), v = 0, i = arry.length; while ( i-- ) { v += Math.pow( ( arry[ i ] - avg ), 2 ); } divisor = ( arry.length - ( population ? 0 : 1 ) ); if ( divisor === 0 ) { return math.invalid( c, 'variance', 0 ); } v /= divisor; return v; }, // variance (population) varp : function( arry, c ) { return ts.equations.variance( arry, true, c ); }, // variance (sample) vars : function( arry, c ) { return ts.equations.variance( arry, false, c ); }, // standard deviation (sample) stdevs : function( arry, c ) { var vars = ts.equations.variance( arry, false, c ); return Math.sqrt( vars ); }, // standard deviation (population) stdevp : function( arry, c ) { var varp = ts.equations.variance( arry, true, c ); return Math.sqrt( varp ); } }; // add new widget called repeatHeaders // ************************************ ts.addWidget({ id: 'math', priority: 100, options: { math_data : 'math', math_debug : false, // column index to ignore math_ignore : [], // mask info: https://code.google.com/p/javascript-number-formatter/ math_mask : '#,##0.00', // complete executed after each fucntion math_complete : null, // function($cell, wo, result, value, arry) { return result; }, // math_completed called after all math calculations have completed math_completed: function( /* config */ ) {}, // order of calculation; 'all' is last math_priority : [ 'row', 'above', 'below', 'col' ], // template for or just prepend the mask prefix & suffix with this HTML // e.g. '{content}' math_prefix : '', math_suffix : '', // no matching math elements found (text added to cell) math_none : 'N/A', math_event : 'recalculate', // use this filter to target specific rows (e.g. ':visible', or ':not(.empty-row)') math_rowFilter: '' }, init : function( table, thisWidget, c, wo ) { // filterEnd fires after updateComplete var update = ( ts.hasWidget( table, 'filter' ) ? 'filterEnd' : 'updateComplete' ) + '.tsmath'; // filterEnd is when the pager hides rows... so bind to pagerComplete math.events += ( ts.hasWidget( table, 'pager' ) ? 'pagerComplete' : 'filterEnd' ) + '.tsmath '; c.$table .off( ( math.events + 'updateComplete.tsmath ' + wo.math_event ).replace( /\s+/g, ' ' ) ) .on( math.events + wo.math_event, function( e ) { if ( !this.hasInitialized ) { return; } var init = e.type === 'tablesorter-initialized'; if ( !wo.math_isUpdating || init ) { // don't setColumnIndexes on init here, or it gets done twice if ( !/filter/.test( e.type ) && !init ) { // redo data-column indexes on update math.setColumnIndexes( c ); } math.recalculate( c, wo, init ); } }) .on( update, function() { setTimeout( function() { math.updateComplete( c ); }, 40 ); }); wo.math_isUpdating = false; // math widget initialized after table - see #946 if ( table.hasInitialized ) { math.recalculate( c, wo, true ); } }, // this remove function is called when using the refreshWidgets method or when destroying the tablesorter plugin // this function only applies to tablesorter v2.4+ remove: function( table, c, wo, refreshing ) { if ( refreshing ) { return; } c.$table .off( ( math.events + ' updateComplete.tsmath ' + wo.math_event ).replace( /\s+/g, ' ' ) ) .children().children( 'tr' ).children( '[data-' + wo.math_data + ']' ).empty(); } }); })(jQuery);