<!DOCTYPE html>
<html ng-app="myApp">
<head>
<link data-require="bootstrap-css@3.2.0" data-semver="3.2.0" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" />
<script data-require="angular.js@1.3.1" data-semver="1.3.1" src="//code.angularjs.org/1.3.1/angular.js"></script>
<script data-require="ui-bootstrap@0.11.0" data-semver="0.11.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.0.min.js"></script>
<script data-require="lodash.js@3.10.0" data-semver="3.10.0" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.0/lodash.js"></script>
<script src="smart-table.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-controller="mainCtrl">
<table st-set-filter="customFilter" st-table="displayed" st-safe-src="collection" class="table table-bordered table-striped">
<thead>
<tr>
<th st-sort="firstName">first name</th>
<th st-sort="lastName">last name</th>
<th st-sort="nationality">nationality</th>
<th st-sort="education">education</th>
</tr>
<tr>
<th>
<input placeholder="search firstname" st-search="firstName" />
</th>
<th>
<input placeholder="search lastname" st-search="lastName" />
</th>
<th>
<st-select-distinct collection="collection" predicate="nationality"></st-select-distinct>
</th>
<th>
<st-select-multiple collection="collection" predicate="education"></st-select-multiple>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in displayed">
<td>{{row.firstName}}</td>
<td>{{row.lastName | uppercase}}</td>
<td>{{row.nationality}}</td>
<td>{{row.education}}</td>
</tr>
</tbody>
<tfoot></tfoot>
</table>
</body>
</html>
(function(ng) {
angular.module('myApp', ['smart-table', 'ui.bootstrap'])
.controller('mainCtrl', ['$scope', function($scope) {
var
nameList = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa'],
familyName = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez'],
nationList = ['USA', 'France', 'Germany'],
educationList = ['Doctorate', 'Master', 'Bachelor', 'High school'];
function createRandomItem() {
var
firstName = nameList[Math.floor(Math.random() * 5)],
lastName = familyName[Math.floor(Math.random() * 5)],
nationality = nationList[Math.floor(Math.random() * 3)],
education = educationList[Math.floor(Math.random() * 4)];
return {
firstName: firstName,
lastName: lastName,
nationality: nationality,
education: education
};
}
$scope.itemsByPage = 15;
$scope.collection = [];
$scope.displayed = [].concat($scope.collection);
for (var j = 0; j < 200; j++) {
$scope.collection.push(createRandomItem());
}
}])
.directive('stSelectDistinct', [function() {
return {
restrict: 'E',
require: '^stTable',
scope: {
collection: '=',
predicate: '@',
predicateExpression: '='
},
template: '<select ng-model="selectedOption" ng-change="optionChanged(selectedOption)" ng-options="opt for opt in distinctItems"></select>',
link: function(scope, element, attr, table) {
var getPredicate = function() {
var predicate = scope.predicate;
if (!predicate && scope.predicateExpression) {
predicate = scope.predicateExpression;
}
return predicate;
}
scope.$watch('collection', function(newValue) {
var predicate = getPredicate();
if (newValue) {
var temp = [];
scope.distinctItems = ['All'];
angular.forEach(scope.collection, function(item) {
var value = item[predicate];
if (value && value.trim().length > 0 && temp.indexOf(value) === -1) {
temp.push(value);
}
});
temp.sort();
scope.distinctItems = scope.distinctItems.concat(temp);
scope.selectedOption = scope.distinctItems[0];
scope.optionChanged(scope.selectedOption);
}
}, true);
scope.optionChanged = function(selectedOption) {
var predicate = getPredicate();
var query = {};
query.distinct = selectedOption;
if (query.distinct === 'All') {
query.distinct = '';
}
table.search(query, predicate);
};
}
}
}])
.directive('stSelectMultiple', [function() {
return {
restrict: 'E',
require: '^stTable',
scope: {
collection: '=',
predicate: '@',
predicateExpression: '='
},
templateUrl: 'stSelectMultiple.html',
link: function(scope, element, attr, table) {
scope.dropdownLabel = '';
scope.filterChanged = filterChanged;
initialize();
function initialize() {
bindCollection(scope.collection);
}
function getPredicate() {
var predicate = scope.predicate;
if (!predicate && scope.predicateExpression) {
predicate = scope.predicateExpression;
}
return predicate;
}
function getDropdownLabel() {
var allCount = scope.distinctItems.length;
var selected = getSelectedOptions();
if (allCount === selected.length || selected.length === 0) {
return 'All';
}
if (selected.length === 1) {
return selected[0];
}
return selected.length + ' items';
}
function getSelectedOptions() {
var selectedOptions = [];
angular.forEach(scope.distinctItems, function(item) {
if (item.selected) {
selectedOptions.push(item.value);
}
});
return selectedOptions;
}
function bindCollection(collection) {
var predicate = getPredicate();
var distinctItems = [];
angular.forEach(collection, function(item) {
var value = item[predicate];
fillDistinctItems(value, distinctItems);
});
distinctItems.sort(function(obj, other) {
if (obj.value > other.value) {
return 1;
} else if (obj.value < other.value) {
return -1;
}
return 0;
});
scope.distinctItems = distinctItems;
filterChanged();
}
function filterChanged() {
scope.dropdownLabel = getDropdownLabel();
var predicate = getPredicate();
var query = {
matchAny: {}
};
query.matchAny.items = getSelectedOptions();
var numberOfItems = query.matchAny.items.length;
if (numberOfItems === 0 || numberOfItems === scope.distinctItems.length) {
query.matchAny.all = true;
} else {
query.matchAny.all = false;
}
table.search(query, predicate);
}
function fillDistinctItems(value, distinctItems) {
if (value && value.trim().length > 0 && !findItemWithValue(distinctItems, value)) {
distinctItems.push({
value: value,
selected: true
});
}
}
function findItemWithValue(collection, value) {
var found = _.find(collection, function(item) {
return item.value === value;
});
return found;
}
}
}
}])
.filter('customFilter', ['$filter', function($filter) {
var filterFilter = $filter('filter');
var standardComparator = function standardComparator(obj, text) {
text = ('' + text).toLowerCase();
return ('' + obj).toLowerCase().indexOf(text) > -1;
};
return function customFilter(array, expression) {
function customComparator(actual, expected) {
var isBeforeActivated = expected.before;
var isAfterActivated = expected.after;
var isLower = expected.lower;
var isHigher = expected.higher;
var higherLimit;
var lowerLimit;
var itemDate;
var queryDate;
if (ng.isObject(expected)) {
//exact match
if (expected.distinct) {
if (!actual || actual.toLowerCase() !== expected.distinct.toLowerCase()) {
return false;
}
return true;
}
//matchAny
if (expected.matchAny) {
if (expected.matchAny.all) {
return true;
}
if (!actual) {
return false;
}
for (var i = 0; i < expected.matchAny.items.length; i++) {
if (actual.toLowerCase() === expected.matchAny.items[i].toLowerCase()) {
return true;
}
}
return false;
}
//date range
if (expected.before || expected.after) {
try {
if (isBeforeActivated) {
higherLimit = expected.before;
itemDate = new Date(actual);
queryDate = new Date(higherLimit);
if (itemDate > queryDate) {
return false;
}
}
if (isAfterActivated) {
lowerLimit = expected.after;
itemDate = new Date(actual);
queryDate = new Date(lowerLimit);
if (itemDate < queryDate) {
return false;
}
}
return true;
} catch (e) {
return false;
}
} else if (isLower || isHigher) {
//number range
if (isLower) {
higherLimit = expected.lower;
if (actual > higherLimit) {
return false;
}
}
if (isHigher) {
lowerLimit = expected.higher;
if (actual < lowerLimit) {
return false;
}
}
return true;
}
//etc
return true;
}
return standardComparator(actual, expected);
}
var output = filterFilter(array, expression, customComparator);
return output;
};
}]);
})(angular);
/* Styles go here */
/**
* @version 2.1.1
* @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
},
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 : 0;
}, function (newValue, oldValue) {
if (newValue !== safeCopy.length) {
updateSafeCopy();
}
});
$scope.$watch(function () {
return safeGetter($scope);
}, function (newValue, oldValue) {
if (newValue !== oldValue) {
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', function (stConfig, $parse) {
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;
if (attr.stSortDefault) {
sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;
}
//view --> table state
function sort () {
index++;
predicate = ng.isFunction(getter(scope)) ? getter(scope) : attr.stSort;
if (index % 3 === 0 && !!skipNatural !== true) {
//manual reset
index = 0;
ctrl.tableState().sort = {};
ctrl.tableState().pagination.start = 0;
ctrl.pipe();
} else {
ctrl.sortBy(predicate, index % 2 === 0);
}
}
element.bind('click', function sortClick () {
if (predicate) {
scope.$apply(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);
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{dropdownLabel}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li ng-repeat="item in distinctItems">
<label>
<input type="checkbox" ng-model="item.selected" ng-click="filterChanged()">{{item.value}}</label>
</li>
</ul>
</div>