CDRParser/static/js/angular-tablesort.js
2016-11-17 15:56:56 -06:00

504 lines
22 KiB
JavaScript

/*
angular-tablesort v1.3.1
(c) 2013-2016 Mattias Holmlund, http://mattiash.github.io/angular-tablesort
License: MIT
*/
var tableSortModule = angular.module( 'tableSort', [] );
tableSortModule.provider( 'tableSortConfig', function () {
this.filterTemplate = ''; //no filtering by default unless a template is provided
this.filterFunction = null; //empty by default - use the built in filter function when left blank
this.paginationTemplate = ''; //no pagination by default unless a template is provided
this.perPageOptions = [10, 25, 50, 100];
this.perPageDefault = this.perPageOptions[0]; //first option by default
this.itemNameSingular = 'item';
this.itemNamePlural = this.itemNameSingular + 's';
this.noDataText = 'No ' + this.itemNamePlural;
if( !isNaN(this.perPageDefault) && this.perPageOptions.indexOf(this.perPageDefault) === -1 ) {
//If a default per-page option was added that isn't in the array, add it and sort the array
this.perPageOptions.push(this.perPageDefault);
}
//Sort the array
this.perPageOptions.sort(function (a,b) {return a - b;});
this.$get = function () {
return this;
};
});
tableSortModule.directive( 'tsWrapper', ['$parse', '$compile', function( $parse, $compile ) {
'use strict';
function replaceTemplateTokens($scope, templateString) {
//Replace some strings with the proper expressions to be compiled
return templateString
.replace(/FILTER_STRING/g, 'filtering.filterString')
.replace(/CURRENT_PAGE_RANGE/g, 'pagination.getPageRangeString(TOTAL_COUNT)')
.replace(/TOTAL_COUNT/g, $scope.itemsArrayExpression + '.length')
.replace(/PER_PAGE_OPTIONS/g, 'pagination.perPageOptions')
.replace(/ITEMS_PER_PAGE/g, 'pagination.perPage')
.replace(/ITEM_NAME_SINGULAR/g, 'itemNameSingular')
.replace(/ITEM_NAME_PLURAL/g, 'itemNamePlural')
.replace(/FILTERED_COUNT/g, 'filtering.filteredCount')
.replace(/CURRENT_PAGE_NUMBER/g, 'pagination.currentPage');
}
return {
scope: true,
controller: ['$scope', 'tableSortConfig', function($scope, tableSortConfig ) {
//local scope vars for this directive
$scope.pagination = {
template: tableSortConfig.paginationTemplate,
perPageOptions: tableSortConfig.perPageOptions.concat(), //copy the array, not a reference
perPage: tableSortConfig.perPageDefault,
currentPage: 1,
getPageRangeString: function(total) {
//TODO: Format these numbers, perhaps optionally
var maxOnPage = total !== $scope.filtering.filteredCount ? $scope.filtering.filteredCount : total;
//This keeps the first page labeled as 1, not 0
var startPage = Math.max(($scope.pagination.currentPage - 1) * $scope.pagination.perPage + 1, 1);
var endPage = Math.min($scope.pagination.currentPage * $scope.pagination.perPage, maxOnPage);
//This prevents the range from showing when the total number of items can be shown on a single page
return $scope.filtering.filteredCount === 0 ? '' : (endPage === maxOnPage && startPage === 1 ? '' : startPage + '-') + endPage;
}
};
$scope.filtering = {
template: tableSortConfig.filterTemplate,
filterString: '',
filterFunction: tableSortConfig.filterFunction,
filteredCount: 0,
filterFields: []
};
$scope.itemsArrayExpression = ''; //this will contain the string expression for the array of items in the table
$scope.itemNameSingular = tableSortConfig.itemNameSingular;
$scope.itemNamePlural = tableSortConfig.itemNamePlural;
$scope.noDataText = tableSortConfig.noDataText;
$scope.sortExpression = [];
$scope.headings = [];
var parse_sortexpr = function( expr, name ) {
return [$parse( expr ), null, false, name ? name : expr];
};
this.setSortField = function( sortexpr, element, name ) {
var i;
var expr = parse_sortexpr( sortexpr, name );
if( $scope.sortExpression.length === 1 && $scope.sortExpression[0][0] === expr[0] ) {
if( $scope.sortExpression[0][2] ) {
element.removeClass( 'tablesort-desc' );
element.addClass( 'tablesort-asc' );
$scope.sortExpression[0][2] = false;
} else {
element.removeClass( 'tablesort-asc' );
element.addClass( 'tablesort-desc' );
$scope.sortExpression[0][2] = true;
}
$scope.$emit( 'tablesort:sortOrder', [{
name: $scope.sortExpression[0][3],
order: $scope.sortExpression[0][2]
}]);
} else {
for( i=0; i<$scope.headings.length; i=i+1 ) {
$scope.headings[i]
.removeClass( 'tablesort-desc' )
.removeClass( 'tablesort-asc' );
}
element.addClass( 'tablesort-asc' );
$scope.sortExpression = [expr];
$scope.$emit( 'tablesort:sortOrder', [{
name: expr[3],
order: expr[2]
}]);
}
};
this.addSortField = function( sortexpr, element, name ) {
var i;
var toggle_order = false;
var expr = parse_sortexpr( sortexpr, name );
for( i=0; i<$scope.sortExpression.length; i=i+1 ) {
if( $scope.sortExpression[i][0] === expr[0] ) {
if( $scope.sortExpression[i][2] ) {
element.removeClass( 'tablesort-desc' );
element.addClass( 'tablesort-asc' );
$scope.sortExpression[i][2] = false;
} else {
element.removeClass( 'tablesort-asc' );
element.addClass( 'tablesort-desc' );
$scope.sortExpression[i][2] = true;
}
toggle_order = true;
}
}
if( !toggle_order ) {
element.addClass( 'tablesort-asc' );
$scope.sortExpression.push( expr );
}
$scope.$emit( 'tablesort:sortOrder', $scope.sortExpression.map(function (a) {
return {
name: a[3],
order: a[2]
};
}));
};
this.setTrackBy = function( trackBy ) {
$scope.trackBy = trackBy;
};
this.registerHeading = function( headingelement ) {
$scope.headings.push( headingelement );
};
this.addFilterField = function( sortexpr, element ) {
var expr = parse_sortexpr( sortexpr );
$scope.filtering.filterFields.push( expr );
};
this.setArrayExpr = function( dataArrayExp ) {
$scope.itemsArrayExpression = dataArrayExp;
};
}],
link: function($scope, $element, $attrs, tsWrapperCtrl) {
if( $attrs.tsItemName ) {
var originalNoDataText = 'No ' + $scope.itemNamePlural;
//if the table attributes has an item name on it, this takes priority
$scope.itemNameSingular = $attrs.tsItemName;
if( $attrs.tsItemNamePlural ) {
//if a plural name was specified, use that
$scope.itemNamePlural = $attrs.tsItemNamePlural;
} else {
//otherwise just add 's' to the singular name
$scope.itemNamePlural = $attrs.tsItemName + 's';
}
if( !$attrs.tsNoDataText && $scope.noDataText === originalNoDataText ) {
//If the noDataText was NOT specified AND it's in the same 'No ITEMS' format as the default , update it to contain the new item name
$scope.noDataText = 'No ' + $scope.itemNamePlural;
}
}
if( $attrs.tsNoDataText ) {
//If the noDataText was specified, update it
$scope.noDataText = $attrs.tsNoDataText;
}
//local attribute usages of the pagination/filtering options will override the global config
if( $attrs.tsPerPageOptions ) {
$scope.pagination.perPageOptions = $scope.$eval($attrs.tsPerPageOptions);
}
if( $attrs.tsPerPageDefault ) {
var defaultPerPage = $scope.$eval($attrs.tsPerPageDefault);
if( !isNaN(defaultPerPage) ) {
$scope.pagination.perPage = defaultPerPage;
if( $scope.pagination.perPageOptions.indexOf($scope.pagination.perPage) === -1 ) {
//If a default per-page option was added that isn't in the array, add it and sort the array
$scope.pagination.perPageOptions.push($scope.pagination.perPage);
$scope.pagination.perPageOptions.sort(function (a,b) {return a - b;});
}
}
}
if( $attrs.tsFilterFields ) {
var filterFields = $attrs.tsFilterFields.split(',')
.filter(function(item) {
return item && item.trim() !== '';
});
for( var i=0; i<filterFields.length; i=i+1 ) {
tsWrapperCtrl.addFilterField(filterFields[i]);
}
}
var $filterHtml;
if( $attrs.tsDisplayFiltering !== 'false' && $scope.filtering.template !== '' && $scope.filtering.filterFields.length>0 ) {
var filterString = replaceTemplateTokens($scope, $scope.filtering.template);
$filterHtml = $compile(filterString)($scope);
//Add filtering HTML BEFORE the table
$element.parent()[0].insertBefore($filterHtml[0], $element[0]);
}
if( $attrs.tsFilterFunction ) {
//if the table attributes has a filter function on it, this takes priority
$scope.filtering.filterFunction = $scope.$eval($attrs.tsFilterFunction);
}
if( !angular.isFunction($scope.filtering.filterFunction) ) {
//No custom filter was provided...
if( $scope.filtering.filterFields.length===0 ) {
//There are no filter fields, so always return everything
$scope.filtering.filterFunction = function(item) {
return true;
};
} else {
//This is the default filter function. It does a lowercase string match
$scope.filtering.filterFunction = function(item) {
var shouldInclude = false;
for( var i=0; i<$scope.filtering.filterFields.length; i=i+1 ) {
if( !shouldInclude ) {
var str = ($scope.filtering.filterFields[i][0](item) || '').toString().toLowerCase(); //parse the item's property using the `ts-criteria` value & filter
shouldInclude = str.indexOf($scope.filtering.filterString.toLowerCase()) > -1;
}
}
return shouldInclude;
};
}
}
$scope.filterLimitFun = function(array) {
if( !$attrs.tsFilterFunction && $scope.filtering.filterString === '' ) {
//Return unfiltered when NOT using a custom filter function and when nothing is being searched
$scope.filtering.filteredCount = array.length;
return array;
}
var filteredArr = array.filter($scope.filtering.filterFunction);
$scope.filtering.filteredCount = filteredArr.length;
return filteredArr;
};
$scope.sortFun = function( a, b ) {
var i, aval, bval, descending, filterFun, compResult;
var collator = new Intl.Collator(undefined, {sensitivity: 'case'});
for( i=0; i<$scope.sortExpression.length; i=i+1 ) {
aval = $scope.sortExpression[i][0](a);
bval = $scope.sortExpression[i][0](b);
filterFun = b[$scope.sortExpression[i][1]];
if( filterFun ) {
aval = filterFun( aval );
bval = filterFun( bval );
}
if( aval === undefined || aval === null ) {
aval = '';
}
if( bval === undefined || bval === null ) {
bval = '';
}
descending = $scope.sortExpression[i][2];
compResult = collator.compare(aval, bval);
if( compResult === 1 ) {
return descending ? -1 : 1;
} else if( compResult === -1 ) {
return descending ? 1 : -1;
}
}
// All the sort fields were equal. If there is a 'track by'' expression,
// use that as a tiebreaker to make the sort result stable.
if( $scope.trackBy ) {
aval = a[$scope.trackBy];
bval = b[$scope.trackBy];
if( aval === undefined || aval === null ) {
aval = '';
}
if( bval === undefined || bval === null ) {
bval = '';
}
compResult = collator.compare(aval, bval);
if( compResult === 1 ) {
return descending ? -1 : 1;
} else if( compResult === -1 ) {
return descending ? 1 : -1;
}
}
return 0;
};
$scope.pageLimitFun = function(array) {
if( $attrs.tsDisplayPagination === 'false' || $scope.pagination.template === '') {
//pagination is disabled on this table or there is no template, so return everything
return array;
}
//Only return the items that are in the correct index range for the currently selected page
var begin = ($scope.pagination.currentPage - 1) * $scope.pagination.perPage;
var end = $scope.pagination.currentPage * $scope.pagination.perPage;
var final=[];
for( var i=0; i < array.length; i++ ) {
if( i >= begin && i < end ) {
final.push(array[i]);
}
}
return final;
};
var $paginationHtml;
if( $attrs.tsDisplayPagination !== 'false' && $scope.pagination.template !== '' ) {
var pagerString = replaceTemplateTokens($scope, $scope.pagination.template);
$paginationHtml = $compile(pagerString)($scope);
//Add pagination HTML AFTER the table
$element.after($paginationHtml);
}
if( $attrs.tsGetTableDataFunction ) {
var getter = $parse($attrs.tsGetTableDataFunction);
var setter = getter.assign;
//If this attribute has a value, then we want to turn it into a function on the parent scope
//so that it can be passed into other functions and run on the parent controllers as needed
var fn = function( shouldApplySorting, shouldApplyFiltering, limitToCurrentPageOnly ) {
var arr = $parse($scope.itemsArrayExpression)($scope);
if( shouldApplySorting ) {
arr = arr.sort($scope.sortFun);
}
if( shouldApplyFiltering ) {
arr = $scope.filterLimitFun(arr);
}
if( limitToCurrentPageOnly ) {
arr = $scope.pageLimitFun(arr);
}
return arr;
};
setter($scope.$parent, fn);
}
$scope.$on( '$destroy', function() {
//When the directive is destroyed, also remove the filter & pagination HTML
if( $filterHtml ) {
$filterHtml.remove();
}
if( $paginationHtml ) {
$paginationHtml.remove();
}
});
}
};
}]);
tableSortModule.directive( 'tsCriteria', function() {
return {
require: '^tsWrapper',
link: function(scope, element, attrs, tsWrapperCtrl) {
var clickingCallback = function(event) {
scope.$apply( function() {
if( event.shiftKey ) {
tsWrapperCtrl.addSortField(attrs.tsCriteria, element, attrs.tsName);
} else {
tsWrapperCtrl.setSortField(attrs.tsCriteria, element, attrs.tsName);
}
} );
};
element.bind( 'click', clickingCallback );
element.addClass( 'tablesort-sortable' );
if( 'tsDefault' in attrs && attrs.tsDefault !== '0' ) {
tsWrapperCtrl.addSortField( attrs.tsCriteria, element, attrs.tsName );
if( attrs.tsDefault === 'descending' ) {
tsWrapperCtrl.addSortField( attrs.tsCriteria, element, attrs.tsName );
}
}
if( 'tsFilter' in attrs) {
tsWrapperCtrl.addFilterField( attrs.tsCriteria, element );
}
tsWrapperCtrl.registerHeading( element );
}
};
});
tableSortModule.directive( 'tsRepeat', ['$compile', '$interpolate', function($compile, $interpolate) {
return {
terminal: true,
multiElement: true,
require: '^tsWrapper',
priority: 1000000,
link: function(scope, element, attrs, tsWrapperCtrl) {
var repeatAttrs = ['ng-repeat', 'data-ng-repeat', 'ng-repeat-start', 'data-ng-repeat-start'];
var ngRepeatDirective = repeatAttrs[0];
var tsRepeatDirective = 'ts-repeat';
for (var i = 0; i < repeatAttrs.length; i++) {
if (angular.isDefined(element.attr(repeatAttrs[i]))) {
ngRepeatDirective = repeatAttrs[i];
tsRepeatDirective = ngRepeatDirective.replace(/^(data-)?ng/, '$1ts');
break;
}
}
var tsExpr = 'tablesortOrderBy:sortFun | tablesortLimit:filterLimitFun | tablesortLimit:pageLimitFun';
var repeatExpr = element.attr(ngRepeatDirective);
var repeatExprRegex = /^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(\s+track\s+by\s+[\s\S]+?)?\s*$/;
var trackByMatch = repeatExpr.match(/\s+track\s+by\s+\S+?\.(\S+)/);
var repeatInMatch = repeatExpr.match(repeatExprRegex);
if (trackByMatch) {
tsWrapperCtrl.setTrackBy(trackByMatch[1]);
}
//Limit Sort the results, then limit them to only include what matches the filter, then only what's on the current page
if (repeatExpr.search(/tablesort/) !== -1) {
repeatExpr = repeatExpr.replace(/tablesort/, tsExpr);
if (trackByMatch) {
//Move the 'track by'' statement to the end
repeatExpr = repeatExpr.replace(trackByMatch[0], '') + trackByMatch[0];
}
} else {
repeatExpr = repeatExpr.replace(repeatExprRegex, '$1 in $2 | ' + tsExpr + '$3');
}
if (angular.isUndefined(attrs.tsHideNoData)) {
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var noDataRow = angular.element(element[0]).clone();
noDataRow.removeAttr(ngRepeatDirective);
noDataRow.removeAttr(tsRepeatDirective);
noDataRow.addClass( 'showIfLast' );
noDataRow.children().remove();
noDataRow.append( '<td colspan="' + element[0].childElementCount + '">' + startSym + 'noDataText' + endSym + '</td>' );
noDataRow = $compile(noDataRow)(scope);
element.parent().prepend(noDataRow);
}
//pass the `itemsList` from `item in itemsList` to the master directive as a string so it can be used in expressions
tsWrapperCtrl.setArrayExpr(repeatInMatch[2]);
angular.element(element[0]).attr(ngRepeatDirective, repeatExpr);
$compile(element, null, 1000000)(scope);
}
};
}]);
tableSortModule.filter( 'tablesortLimit', function() {
return function(array, limitFun) {
if(!array) return;
return limitFun(array);
};
} );
tableSortModule.filter( 'tablesortOrderBy', function() {
return function(array, sortfun ) {
if(!array) return;
var arrayCopy = array.concat();
return arrayCopy.sort( sortfun );
};
} );
tableSortModule.filter( 'parseInt', function() {
return function(input) {
return parseInt( input ) || null;
};
} );
tableSortModule.filter( 'parseFloat', function() {
return function(input) {
return parseFloat( input ) || null;
};
} );
tableSortModule.filter( 'parseDate', function () {
return function (input) {
var timestamp = Date.parse(input);
return isNaN(timestamp) ? null : timestamp;
};
} );