<!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.2.21" data-semver="1.2.21" src="https://code.angularjs.org/1.2.21/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
<script src="smart-table.debug.js"></script>
<script src="lrInfiniteScrollPlugin.js"></script>
</head>
<body ng-controller="mainCtrl">
<div class="table-container">
<table st-table="rowCollection" st-pagination-scroll st-pipe="callServer" class="table table-striped">
<thead>
<tr>
<th st-sort="firstName">first name</th>
<th st-sort="lastName">last name</th>
<th st-sort="birthDate">birth date</th>
<th st-sort="balance">balance</th>
<th>email</th>
</tr>
<tr>
<th>
<input st-search="firstName" placeholder="search for firstname" class="input-sm form-control" type="search"/>
</th>
<th colspan="4">
<input st-search placeholder="global search" class="input-sm form-control" type="search"/>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in rowCollection">
<td>{{row.firstName | uppercase}}</td>
<td>{{row.lastName}}</td>
<td>{{row.birthDate | date}}</td>
<td>{{row.balance | currency}}</td>
<td><a ng-href="mailto:{{row.email}}">email</a></td>
</tr>
</tbody>
</table>
</div>
<div ng-show="isLoading" class="loading-indicator"></div>
</body>
</html>
angular.module('myApp', ['smart-table'])
.controller('mainCtrl', ['$scope', '$timeout',
function ($scope, $timeout) {
var nameList = ['Pierre', 'Pol', 'Jacques', 'Robert', 'Elisa'];
var familyName = ['Dupont', 'Germain', 'Delcourt', 'bjip', 'Menez'];
$scope.isLoading = false;
$scope.rowCollection = [];
function createRandomItem() {
var
firstName = nameList[Math.floor(Math.random() * 4)],
lastName = familyName[Math.floor(Math.random() * 4)],
age = Math.floor(Math.random() * 100),
email = firstName + lastName + '@whatever.com',
balance = Math.random() * 3000;
return {
firstName: firstName,
lastName: lastName,
age: age,
email: email,
balance: balance
};
}
function getAPage() {
var data = [];
for (var j = 0; j < 20; j++) {
data.push(createRandomItem());
}
return data;
}
var lastStart = 0;
var maxNodes = 40;
$scope.callServer = function getData(tableState) {
//here you could create a query string from tableState
//fake ajax call
$scope.isLoading = true;
$timeout(function () {
//if we reset (like after a search or an order)
if (tableState.pagination.start === 0) {
$scope.rowCollection = getAPage();
} else {
//we load more
$scope.rowCollection = $scope.rowCollection.concat(getAPage());
//remove first nodes if needed
if (lastStart < tableState.pagination.start && $scope.rowCollection.length > maxNodes) {
//remove the first nodes
$scope.rowCollection.splice(0, 20);
}
}
lastStart = tableState.pagination.start;
$scope.isLoading = false;
}, 1000);
};
$scope.rowCollection = getAPage();
}
]);
.st-sort-ascent:before {
content: '\25B2';
}
.st-sort-descent:before {
content: '\25BC';
}
.table-container{
height:500px;
overflow-y:scroll;
}
.loading-indicator {
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
width: 100%;
text-align: center;
padding: 0.7em; }
.loading-indicator:before {
display: inline-block;
margin: 0 0.4em;
min-width: 1em;
min-height: 1em;
border-top: 4px solid #646464;
border-right: 4px solid #e6e6e6;
border-left: 4px solid #e6e6e6;
border-bottom: 4px solid #646464;
content: "";
-webkit-animation: halfspin 1s ease infinite;
-moz-animation: halfspin 1s ease infinite;
-o-animation: halfspin 1s ease infinite;
animation: halfspin 1s ease infinite;
border-radius: 100%; }
@-webkit-keyframes halfspin {
to {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
transform: rotate(180deg); } }
@-moz-keyframes halfspin {
to {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
transform: rotate(180deg); } }
@keyframes halfspin {
to {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
transform: rotate(180deg); } }
(function (ng) {
'use strict';
ng.module('smart-table',[]);
})(angular);
(function (ng, undefined) {
'use strict';
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
}
};
var pipeAfterSafeCopy = true;
var ctrl = this;
function copyRefs(src) {
return [].concat(src);
}
function updateSafeCopy() {
safeCopy = copyRefs(safeGetter($scope));
if (pipeAfterSafeCopy === true) {
ctrl.pipe();
}
}
if ($attrs.stSafeSrc) {
safeGetter = $parse($attrs.stSafeSrc);
$scope.$watch(function () {
var safeSrc = safeGetter($scope);
return safeSrc ? safeSrc.length : 0;
}, function (newValue, oldValue) {
if (newValue !== oldValue) {
updateSafeCopy()
}
});
$scope.$watch(function () {
return safeGetter($scope);
}, function (newValue, oldValue) {
if (newValue !== oldValue) {
updateSafeCopy();
}
});
}
/**
* sort the rows
* @param predicate function or string which will be used as predicate for the sorting
* @param [optional] reverse if you want to reverse the order
*/
this.sortBy = function sortBy(predicate, reverse) {
tableState.sort.predicate = predicate;
tableState.sort.reverse = reverse === true;
tableState.pagination.start = 0;
this.pipe();
};
/**
* search matching rows
* @param input the input string
* @param predicate [optional] 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 : '$';
predicateObject[prop] = input;
tableState.search.predicateObject = predicateObject;
tableState.pagination.start = 0;
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 filtered = tableState.search.predicateObject ? filter(safeCopy, tableState.search.predicateObject) : safeCopy;
filtered = orderBy(filtered, tableState.sort.predicate, tableState.sort.reverse);
if (tableState.pagination.number !== undefined) {
tableState.pagination.numberOfPages = filtered.length > 0 ? Math.ceil(filtered.length / tableState.pagination.number) : 1;
filtered = filtered.slice(tableState.pagination.start, tableState.pagination.start + tableState.pagination.number);
}
displaySetter($scope, filtered);
};
/**
* select a dataRow (it will add the attribute isSelected to the row object)
* @param row the row to select
* @param mode "single" or "multiple"
*/
this.select = function select(row, mode) {
var rows = displayGetter($scope);
var index = rows.indexOf(row);
if (index !== -1) {
if (mode === 'single') {
ng.forEach(displayGetter($scope), function (value, key) {
value.isSelected = key === index ? !value.isSelected : false;
});
} else {
rows[index].isSelected = !rows[index].isSelected;
}
}
};
/**
* take a slice of the current sorted/filtered collection (pagination)
*
* @param start index of the slice
* @param number the number of item in the slice
*/
this.slice = function splice(start, number) {
tableState.pagination.start = start;
tableState.pagination.number = number;
this.pipe();
};
/**
* return the current state of the table
* @returns {{sort: {}, search: {}, pagination: {start: number}}}
*/
this.tableState = function getTableState() {
return tableState;
};
/**
* 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);
};
/**
*User 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) {
}
};
});
})(angular);
(function (ng) {
'use strict';
ng.module('smart-table')
.directive('stSearch', ['$timeout', function ($timeout) {
return {
replace: true,
require: '^stTable',
scope: {
predicate: '=?stSearch'
},
link: function (scope, element, attr, ctrl) {
var tableCtrl = ctrl;
var promise = null;
var throttle = attr.stDelay || 400;
scope.$watch('predicate', function (newValue, oldValue) {
if (newValue !== oldValue) {
ctrl.tableState().search = {};
tableCtrl.search(element[0].value || '', newValue);
}
});
element.bind('input', function (evt) {
evt = evt.originalEvent || evt;
if (promise !== null) {
$timeout.cancel(promise);
}
promise = $timeout(function () {
tableCtrl.search(evt.target.value, scope.predicate || '');
promise = null;
}, throttle);
});
}
}
}])
})(angular);
(function (ng) {
'use strict';
ng.module('smart-table')
.directive('stSelectRow', function () {
return {
restrict: 'A',
require: '^stTable',
scope: {
row: '=stSelectRow'
},
link: function (scope, element, attr, ctrl) {
var mode = attr.stSelectMode || 'single';
element.bind('click', function () {
scope.$apply(function () {
ctrl.select(scope.row, mode);
});
});
scope.$watch('row.isSelected', function (newValue, oldValue) {
if (newValue === true) {
element.addClass('st-selected');
} else {
element.removeClass('st-selected');
}
});
}
}
});
})(angular);
(function (ng) {
'use strict';
ng.module('smart-table')
.directive('stSort', ['$parse', function ($parse) {
return {
restrict: 'A',
require: '^stTable',
link: function (scope, element, attr, ctrl) {
var predicate = attr.stSort;
var getter = $parse(predicate);
var index = 0;
var states = ['descent', 'ascent', 'natural'];
function reset() {
index = 0;
element
.removeClass('st-sort-ascent')
.removeClass('st-sort-descent');
}
if (ng.isFunction(getter(scope))) {
predicate = getter(scope);
}
element.bind('click', function sortClick() {
if (predicate) {
scope.$apply(function () {
index++;
var stateIndex = index % 2;
if (index % 3 === 0) {
//manual reset
ctrl.tableState().sort = {};
ctrl.tableState().pagination.start = 0;
} else {
ctrl.sortBy(predicate, stateIndex === 0);
element
.removeClass('st-sort-' + states[(stateIndex + 1) % 2])
.addClass('st-sort-' + states[stateIndex]);
}
});
}
});
scope.$watch(function () {
return ctrl.tableState().sort;
}, function (newValue, oldValue) {
if (newValue !== oldValue) {
if (newValue.predicate !== predicate) {
reset();
}
}
}, true);
}
};
}])
})(angular);
(function (ng) {
'use strict';
ng.module('smart-table')
.directive('stPagination', function () {
return {
restrict: 'EA',
require: '^stTable',
scope: {},
template: '<div class="pagination"><ul class="pagination"><li ng-repeat="page in pages" ng-class="{active: page==currentPage}"><a ng-click="selectPage(page)">{{page}}</a></li></ul></div>',
replace: true,
link: function (scope, element, attrs, ctrl) {
function isNotNan(value) {
return !(typeof value === 'number' && isNaN(value));
}
var itemsByPage = isNotNan(parseInt(attrs.stItemsByPage, 10)) == true ? parseInt(attrs.stItemsByPage, 10) : 10;
var displayedPages = isNotNan(parseInt(attrs.stDisplayedPages, 10)) == true ? parseInt(attrs.stDisplayedPages, 10) : 5;
scope.currentPage = 1;
scope.pages = [];
scope.$watch(function () {
return ctrl.tableState().pagination;
},
function () {
var paginationState = ctrl.tableState().pagination;
var start = 1;
var end;
var i;
scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;
start = Math.max(start, scope.currentPage - Math.abs(Math.floor(displayedPages / 2)));
end = start + displayedPages;
if (end > paginationState.numberOfPages) {
end = paginationState.numberOfPages + 1;
start = Math.max(1, end - displayedPages);
}
scope.pages = [];
scope.numPages = paginationState.numberOfPages;
for (i = start; i < end; i++) {
scope.pages.push(i);
}
}, true);
scope.selectPage = function (page) {
if (page > 0 && page <= scope.numPages) {
ctrl.slice((page - 1) * itemsByPage, itemsByPage);
}
};
//select the first page
ctrl.slice(0, itemsByPage);
}
};
});
})(angular);
(function (ng) {
'use strict';
ng.module('smart-table')
.directive('stPipe', function () {
return {
require: 'stTable',
scope: {
stPipe: '='
},
link: function (scope, element, attrs, ctrl) {
if (ng.isFunction(scope.stPipe)) {
ctrl.preventPipeOnWatch();
ctrl.pipe = scope.stPipe.bind(ctrl, ctrl.tableState());
}
}
};
});
})(angular);
angular.module('smart-table')
.directive('stPaginationScroll', ['$timeout', function (timeout) {
return{
require: 'stTable',
link: function (scope, element, attr, ctrl) {
var itemByPage = 20;
var pagination = ctrl.tableState().pagination;
var lengthThreshold = 50;
var timeThreshold = 400;
var handler = function () {
//call next page
ctrl.slice(pagination.start + itemByPage, itemByPage);
};
var promise = null;
var lastRemaining = 9999;
var container = angular.element(element.parent());
container.bind('scroll', function () {
var remaining = container[0].scrollHeight - (container[0].clientHeight + container[0].scrollTop);
//if we have reached the threshold and we scroll down
if (remaining < lengthThreshold && (remaining - lastRemaining) < 0) {
//if there is already a timer running which has no expired yet we have to cancel it and restart the timer
if (promise !== null) {
timeout.cancel(promise);
}
promise = timeout(function () {
handler();
//scroll a bit up
container[0].scrollTop -= 500;
promise = null;
}, timeThreshold);
}
lastRemaining = remaining;
});
}
};
}]);