527 lines
16 KiB
JavaScript
527 lines
16 KiB
JavaScript
/**
|
|
* @version 2.1.5
|
|
* @license MIT
|
|
*/
|
|
(function (ng, undefined){
|
|
'use strict';
|
|
|
|
ng.module('smart-table', []).run(['$templateCache', function ($templateCache) {
|
|
$templateCache.put('template/smart-table/pagination.html',
|
|
'<nav ng-if="numPages && pages.length >= 2"><ul class="pagination">' +
|
|
'<li ng-repeat="page in pages" ng-class="{active: page==currentPage}"><a ng-click="selectPage(page)">{{page}}</a></li>' +
|
|
'</ul></nav>');
|
|
}]);
|
|
|
|
|
|
ng.module('smart-table')
|
|
.constant('stConfig', {
|
|
pagination: {
|
|
template: 'template/smart-table/pagination.html',
|
|
itemsByPage: 10,
|
|
displayedPages: 5
|
|
},
|
|
search: {
|
|
delay: 400, // ms
|
|
inputEvent: 'input'
|
|
},
|
|
select: {
|
|
mode: 'single',
|
|
selectedClass: 'st-selected'
|
|
},
|
|
sort: {
|
|
ascentClass: 'st-sort-ascent',
|
|
descentClass: 'st-sort-descent',
|
|
skipNatural: false,
|
|
delay:300
|
|
},
|
|
pipe: {
|
|
delay: 100 //ms
|
|
}
|
|
});
|
|
ng.module('smart-table')
|
|
.controller('stTableController', ['$scope', '$parse', '$filter', '$attrs', function StTableController ($scope, $parse, $filter, $attrs) {
|
|
var propertyName = $attrs.stTable;
|
|
var displayGetter = $parse(propertyName);
|
|
var displaySetter = displayGetter.assign;
|
|
var safeGetter;
|
|
var orderBy = $filter('orderBy');
|
|
var filter = $filter('filter');
|
|
var safeCopy = copyRefs(displayGetter($scope));
|
|
var tableState = {
|
|
sort: {},
|
|
search: {},
|
|
pagination: {
|
|
start: 0,
|
|
totalItemCount: 0
|
|
}
|
|
};
|
|
var filtered;
|
|
var pipeAfterSafeCopy = true;
|
|
var ctrl = this;
|
|
var lastSelected;
|
|
|
|
function copyRefs (src) {
|
|
return src ? [].concat(src) : [];
|
|
}
|
|
|
|
function updateSafeCopy () {
|
|
safeCopy = copyRefs(safeGetter($scope));
|
|
if (pipeAfterSafeCopy === true) {
|
|
ctrl.pipe();
|
|
}
|
|
}
|
|
|
|
function deepDelete (object, path) {
|
|
if (path.indexOf('.') != -1) {
|
|
var partials = path.split('.');
|
|
var key = partials.pop();
|
|
var parentPath = partials.join('.');
|
|
var parentObject = $parse(parentPath)(object)
|
|
delete parentObject[key];
|
|
if (Object.keys(parentObject).length == 0) {
|
|
deepDelete(object, parentPath);
|
|
}
|
|
} else {
|
|
delete object[path];
|
|
}
|
|
}
|
|
|
|
if ($attrs.stSafeSrc) {
|
|
safeGetter = $parse($attrs.stSafeSrc);
|
|
$scope.$watch(function () {
|
|
var safeSrc = safeGetter($scope);
|
|
return safeSrc && safeSrc.length ? safeSrc[0] : undefined;
|
|
}, function (newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
updateSafeCopy();
|
|
}
|
|
});
|
|
$scope.$watch(function () {
|
|
var safeSrc = safeGetter($scope);
|
|
return safeSrc ? safeSrc.length : 0;
|
|
}, function (newValue, oldValue) {
|
|
if (newValue !== safeCopy.length) {
|
|
updateSafeCopy();
|
|
}
|
|
});
|
|
$scope.$watch(function () {
|
|
return safeGetter($scope);
|
|
}, function (newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
tableState.pagination.start = 0;
|
|
updateSafeCopy();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* sort the rows
|
|
* @param {Function | String} predicate - function or string which will be used as predicate for the sorting
|
|
* @param [reverse] - if you want to reverse the order
|
|
*/
|
|
this.sortBy = function sortBy (predicate, reverse) {
|
|
tableState.sort.predicate = predicate;
|
|
tableState.sort.reverse = reverse === true;
|
|
|
|
if (ng.isFunction(predicate)) {
|
|
tableState.sort.functionName = predicate.name;
|
|
} else {
|
|
delete tableState.sort.functionName;
|
|
}
|
|
|
|
tableState.pagination.start = 0;
|
|
return this.pipe();
|
|
};
|
|
|
|
/**
|
|
* search matching rows
|
|
* @param {String} input - the input string
|
|
* @param {String} [predicate] - the property name against you want to check the match, otherwise it will search on all properties
|
|
*/
|
|
this.search = function search (input, predicate) {
|
|
var predicateObject = tableState.search.predicateObject || {};
|
|
var prop = predicate ? predicate : '$';
|
|
|
|
input = ng.isString(input) ? input.trim() : input;
|
|
$parse(prop).assign(predicateObject, input);
|
|
// to avoid to filter out null value
|
|
if (!input) {
|
|
deepDelete(predicateObject, prop);
|
|
}
|
|
tableState.search.predicateObject = predicateObject;
|
|
tableState.pagination.start = 0;
|
|
return this.pipe();
|
|
};
|
|
|
|
/**
|
|
* this will chain the operations of sorting and filtering based on the current table state (sort options, filtering, ect)
|
|
*/
|
|
this.pipe = function pipe () {
|
|
var pagination = tableState.pagination;
|
|
var output;
|
|
filtered = tableState.search.predicateObject ? filter(safeCopy, tableState.search.predicateObject) : safeCopy;
|
|
if (tableState.sort.predicate) {
|
|
filtered = orderBy(filtered, tableState.sort.predicate, tableState.sort.reverse);
|
|
}
|
|
pagination.totalItemCount = filtered.length;
|
|
if (pagination.number !== undefined) {
|
|
pagination.numberOfPages = filtered.length > 0 ? Math.ceil(filtered.length / pagination.number) : 1;
|
|
pagination.start = pagination.start >= filtered.length ? (pagination.numberOfPages - 1) * pagination.number : pagination.start;
|
|
output = filtered.slice(pagination.start, pagination.start + parseInt(pagination.number));
|
|
}
|
|
displaySetter($scope, output || filtered);
|
|
};
|
|
|
|
/**
|
|
* select a dataRow (it will add the attribute isSelected to the row object)
|
|
* @param {Object} row - the row to select
|
|
* @param {String} [mode] - "single" or "multiple" (multiple by default)
|
|
*/
|
|
this.select = function select (row, mode) {
|
|
var rows = copyRefs(displayGetter($scope));
|
|
var index = rows.indexOf(row);
|
|
if (index !== -1) {
|
|
if (mode === 'single') {
|
|
row.isSelected = row.isSelected !== true;
|
|
if (lastSelected) {
|
|
lastSelected.isSelected = false;
|
|
}
|
|
lastSelected = row.isSelected === true ? row : undefined;
|
|
} else {
|
|
rows[index].isSelected = !rows[index].isSelected;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* take a slice of the current sorted/filtered collection (pagination)
|
|
*
|
|
* @param {Number} start - start index of the slice
|
|
* @param {Number} number - the number of item in the slice
|
|
*/
|
|
this.slice = function splice (start, number) {
|
|
tableState.pagination.start = start;
|
|
tableState.pagination.number = number;
|
|
return this.pipe();
|
|
};
|
|
|
|
/**
|
|
* return the current state of the table
|
|
* @returns {{sort: {}, search: {}, pagination: {start: number}}}
|
|
*/
|
|
this.tableState = function getTableState () {
|
|
return tableState;
|
|
};
|
|
|
|
this.getFilteredCollection = function getFilteredCollection () {
|
|
return filtered || safeCopy;
|
|
};
|
|
|
|
/**
|
|
* Use a different filter function than the angular FilterFilter
|
|
* @param filterName the name under which the custom filter is registered
|
|
*/
|
|
this.setFilterFunction = function setFilterFunction (filterName) {
|
|
filter = $filter(filterName);
|
|
};
|
|
|
|
/**
|
|
* Use a different function than the angular orderBy
|
|
* @param sortFunctionName the name under which the custom order function is registered
|
|
*/
|
|
this.setSortFunction = function setSortFunction (sortFunctionName) {
|
|
orderBy = $filter(sortFunctionName);
|
|
};
|
|
|
|
/**
|
|
* Usually when the safe copy is updated the pipe function is called.
|
|
* Calling this method will prevent it, which is something required when using a custom pipe function
|
|
*/
|
|
this.preventPipeOnWatch = function preventPipe () {
|
|
pipeAfterSafeCopy = false;
|
|
};
|
|
}])
|
|
.directive('stTable', function () {
|
|
return {
|
|
restrict: 'A',
|
|
controller: 'stTableController',
|
|
link: function (scope, element, attr, ctrl) {
|
|
|
|
if (attr.stSetFilter) {
|
|
ctrl.setFilterFunction(attr.stSetFilter);
|
|
}
|
|
|
|
if (attr.stSetSort) {
|
|
ctrl.setSortFunction(attr.stSetSort);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
ng.module('smart-table')
|
|
.directive('stSearch', ['stConfig', '$timeout','$parse', function (stConfig, $timeout, $parse) {
|
|
return {
|
|
require: '^stTable',
|
|
link: function (scope, element, attr, ctrl) {
|
|
var tableCtrl = ctrl;
|
|
var promise = null;
|
|
var throttle = attr.stDelay || stConfig.search.delay;
|
|
var event = attr.stInputEvent || stConfig.search.inputEvent;
|
|
|
|
attr.$observe('stSearch', function (newValue, oldValue) {
|
|
var input = element[0].value;
|
|
if (newValue !== oldValue && input) {
|
|
ctrl.tableState().search = {};
|
|
tableCtrl.search(input, newValue);
|
|
}
|
|
});
|
|
|
|
//table state -> view
|
|
scope.$watch(function () {
|
|
return ctrl.tableState().search;
|
|
}, function (newValue, oldValue) {
|
|
var predicateExpression = attr.stSearch || '$';
|
|
if (newValue.predicateObject && $parse(predicateExpression)(newValue.predicateObject) !== element[0].value) {
|
|
element[0].value = $parse(predicateExpression)(newValue.predicateObject) || '';
|
|
}
|
|
}, true);
|
|
|
|
// view -> table state
|
|
element.bind(event, function (evt) {
|
|
evt = evt.originalEvent || evt;
|
|
if (promise !== null) {
|
|
$timeout.cancel(promise);
|
|
}
|
|
|
|
promise = $timeout(function () {
|
|
tableCtrl.search(evt.target.value, attr.stSearch || '');
|
|
promise = null;
|
|
}, throttle);
|
|
});
|
|
}
|
|
};
|
|
}]);
|
|
|
|
ng.module('smart-table')
|
|
.directive('stSelectRow', ['stConfig', function (stConfig) {
|
|
return {
|
|
restrict: 'A',
|
|
require: '^stTable',
|
|
scope: {
|
|
row: '=stSelectRow'
|
|
},
|
|
link: function (scope, element, attr, ctrl) {
|
|
var mode = attr.stSelectMode || stConfig.select.mode;
|
|
element.bind('click', function () {
|
|
scope.$apply(function () {
|
|
ctrl.select(scope.row, mode);
|
|
});
|
|
});
|
|
|
|
scope.$watch('row.isSelected', function (newValue) {
|
|
if (newValue === true) {
|
|
element.addClass(stConfig.select.selectedClass);
|
|
} else {
|
|
element.removeClass(stConfig.select.selectedClass);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}]);
|
|
|
|
ng.module('smart-table')
|
|
.directive('stSort', ['stConfig', '$parse', '$timeout', function (stConfig, $parse, $timeout) {
|
|
return {
|
|
restrict: 'A',
|
|
require: '^stTable',
|
|
link: function (scope, element, attr, ctrl) {
|
|
|
|
var predicate = attr.stSort;
|
|
var getter = $parse(predicate);
|
|
var index = 0;
|
|
var classAscent = attr.stClassAscent || stConfig.sort.ascentClass;
|
|
var classDescent = attr.stClassDescent || stConfig.sort.descentClass;
|
|
var stateClasses = [classAscent, classDescent];
|
|
var sortDefault;
|
|
var skipNatural = attr.stSkipNatural !== undefined ? attr.stSkipNatural : stConfig.sort.skipNatural;
|
|
var promise = null;
|
|
var throttle = attr.stDelay || stConfig.sort.delay;
|
|
|
|
if (attr.stSortDefault) {
|
|
sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;
|
|
}
|
|
|
|
//view --> table state
|
|
function sort () {
|
|
index++;
|
|
var func;
|
|
predicate = ng.isFunction(getter(scope)) || ng.isArray(getter(scope)) ? getter(scope) : attr.stSort;
|
|
if (index % 3 === 0 && !!skipNatural !== true) {
|
|
//manual reset
|
|
index = 0;
|
|
ctrl.tableState().sort = {};
|
|
ctrl.tableState().pagination.start = 0;
|
|
func = ctrl.pipe.bind(ctrl);
|
|
} else {
|
|
func = ctrl.sortBy.bind(ctrl, predicate, index % 2 === 0);
|
|
}
|
|
if (promise !== null) {
|
|
$timeout.cancel(promise);
|
|
}
|
|
if (throttle < 0) {
|
|
scope.$apply(func);
|
|
} else {
|
|
promise = $timeout(func, throttle);
|
|
}
|
|
}
|
|
|
|
element.bind('click', function sortClick () {
|
|
if (predicate) {
|
|
sort();
|
|
}
|
|
});
|
|
|
|
if (sortDefault) {
|
|
index = sortDefault === 'reverse' ? 1 : 0;
|
|
sort();
|
|
}
|
|
|
|
//table state --> view
|
|
scope.$watch(function () {
|
|
return ctrl.tableState().sort;
|
|
}, function (newValue) {
|
|
if (newValue.predicate !== predicate) {
|
|
index = 0;
|
|
element
|
|
.removeClass(classAscent)
|
|
.removeClass(classDescent);
|
|
} else {
|
|
index = newValue.reverse === true ? 2 : 1;
|
|
element
|
|
.removeClass(stateClasses[index % 2])
|
|
.addClass(stateClasses[index - 1]);
|
|
}
|
|
}, true);
|
|
}
|
|
};
|
|
}]);
|
|
|
|
ng.module('smart-table')
|
|
.directive('stPagination', ['stConfig', function (stConfig) {
|
|
return {
|
|
restrict: 'EA',
|
|
require: '^stTable',
|
|
scope: {
|
|
stItemsByPage: '=?',
|
|
stDisplayedPages: '=?',
|
|
stPageChange: '&'
|
|
},
|
|
templateUrl: function (element, attrs) {
|
|
if (attrs.stTemplate) {
|
|
return attrs.stTemplate;
|
|
}
|
|
return stConfig.pagination.template;
|
|
},
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
scope.stItemsByPage = scope.stItemsByPage ? +(scope.stItemsByPage) : stConfig.pagination.itemsByPage;
|
|
scope.stDisplayedPages = scope.stDisplayedPages ? +(scope.stDisplayedPages) : stConfig.pagination.displayedPages;
|
|
|
|
scope.currentPage = 1;
|
|
scope.pages = [];
|
|
|
|
function redraw () {
|
|
var paginationState = ctrl.tableState().pagination;
|
|
var start = 1;
|
|
var end;
|
|
var i;
|
|
var prevPage = scope.currentPage;
|
|
scope.totalItemCount = paginationState.totalItemCount;
|
|
scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;
|
|
|
|
start = Math.max(start, scope.currentPage - Math.abs(Math.floor(scope.stDisplayedPages / 2)));
|
|
end = start + scope.stDisplayedPages;
|
|
|
|
if (end > paginationState.numberOfPages) {
|
|
end = paginationState.numberOfPages + 1;
|
|
start = Math.max(1, end - scope.stDisplayedPages);
|
|
}
|
|
|
|
scope.pages = [];
|
|
scope.numPages = paginationState.numberOfPages;
|
|
|
|
for (i = start; i < end; i++) {
|
|
scope.pages.push(i);
|
|
}
|
|
|
|
if (prevPage !== scope.currentPage) {
|
|
scope.stPageChange({newPage: scope.currentPage});
|
|
}
|
|
}
|
|
|
|
//table state --> view
|
|
scope.$watch(function () {
|
|
return ctrl.tableState().pagination;
|
|
}, redraw, true);
|
|
|
|
//scope --> table state (--> view)
|
|
scope.$watch('stItemsByPage', function (newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
scope.selectPage(1);
|
|
}
|
|
});
|
|
|
|
scope.$watch('stDisplayedPages', redraw);
|
|
|
|
//view -> table state
|
|
scope.selectPage = function (page) {
|
|
if (page > 0 && page <= scope.numPages) {
|
|
ctrl.slice((page - 1) * scope.stItemsByPage, scope.stItemsByPage);
|
|
}
|
|
};
|
|
|
|
if (!ctrl.tableState().pagination.number) {
|
|
ctrl.slice(0, scope.stItemsByPage);
|
|
}
|
|
}
|
|
};
|
|
}]);
|
|
|
|
ng.module('smart-table')
|
|
.directive('stPipe', ['stConfig', '$timeout', function (config, $timeout) {
|
|
return {
|
|
require: 'stTable',
|
|
scope: {
|
|
stPipe: '='
|
|
},
|
|
link: {
|
|
|
|
pre: function (scope, element, attrs, ctrl) {
|
|
|
|
var pipePromise = null;
|
|
|
|
if (ng.isFunction(scope.stPipe)) {
|
|
ctrl.preventPipeOnWatch();
|
|
ctrl.pipe = function () {
|
|
|
|
if (pipePromise !== null) {
|
|
$timeout.cancel(pipePromise)
|
|
}
|
|
|
|
pipePromise = $timeout(function () {
|
|
scope.stPipe(ctrl.tableState(), ctrl);
|
|
}, config.pipe.delay);
|
|
|
|
return pipePromise;
|
|
}
|
|
}
|
|
},
|
|
|
|
post: function (scope, element, attrs, ctrl) {
|
|
ctrl.pipe();
|
|
}
|
|
}
|
|
};
|
|
}]);
|
|
|
|
})(angular); |