/** * @version 2.1.9 * @license MIT */ (function (ng, undefined){ 'use strict'; ng.module('smart-table', []).run(['$templateCache', function ($templateCache) { $templateCache.put('template/smart-table/pagination.html', ''); }]); 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', descendingFirst: false, 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 * @param {String | Function } [comparator] - a comparator to pass to the filter for the (pass true for stric mode) */ this.search = function search(input, predicate, comparator) { 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 descendingFirst = attr.stDescendingFirst !== undefined ? attr.stDescendingFirst : stConfig.sort.descendingFirst; 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 () { if (descendingFirst) { index = index === 0 ? 2 : index - 1; } else { 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) { func(); } else { promise = $timeout(function(){ func(); }, throttle); } } 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);