├── src
├── bottom.txt
├── top.txt
├── smart-table.module.js
├── stConfig.js
├── stSelectRow.js
├── stPipe.js
├── stSearch.js
├── stPagination.js
├── stSort.js
└── stTable.js
├── test
├── init.js
├── karma.conf.js
├── karma.common.js
└── spec
│ ├── stPipe.spec.js
│ ├── stSelectRow.spec.js
│ ├── stTable.spec.js
│ ├── stSearch.spec.js
│ ├── stPagination.spec.js
│ └── stSort.spec.js
├── index.js
├── .travis.yml
├── .gitignore
├── bower.json
├── package.json
├── ISSUE_TEMPLATE.md
├── gulpFile.js
├── readme.md
├── changeLog.md
└── dist
├── smart-table.min.js
├── smart-table.js
└── smart-table.min.js.map
/src/bottom.txt:
--------------------------------------------------------------------------------
1 | })(angular);
--------------------------------------------------------------------------------
/test/init.js:
--------------------------------------------------------------------------------
1 | ng = angular;
--------------------------------------------------------------------------------
/src/top.txt:
--------------------------------------------------------------------------------
1 | (function (ng, undefined){
2 | 'use strict';
3 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('./dist/smart-table.js');
2 | module.exports = 'smart-table';
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 |
5 | before_script:
6 | - npm install
7 |
8 | script:
9 | - gulp build
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | example-app
4 | bower_components
5 | coverage
6 | .DS_Store
7 | test/.DS_Storesmart-table-website
8 |
--------------------------------------------------------------------------------
/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | var conf = require('./karma.common.js');
3 | conf.basePath = '../';
4 | config.set(conf);
5 | };
6 |
--------------------------------------------------------------------------------
/src/smart-table.module.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table', []).run(['$templateCache', function ($templateCache) {
2 | $templateCache.put('template/smart-table/pagination.html',
3 | ' ');
6 | }]);
7 |
8 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-smart-table",
3 | "description": "smart table component for angular v < 2.0",
4 | "main": "./dist/smart-table.js",
5 | "authors": [
6 | "Laurent Renard"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "smart-table",
11 | "table",
12 | "grid",
13 | "angular"
14 | ],
15 | "homepage": "https://github.com/lorenzofox3/Smart-Table",
16 | "ignore": [
17 | "**/.*",
18 | "node_modules",
19 | "bower_components",
20 | "test",
21 | "tests"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/test/karma.common.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | files: [
4 | 'node_modules/jquery/dist/jquery.js',
5 | 'node_modules/angular/angular.js',
6 | 'node_modules/angular-mocks/angular-mocks.js',
7 | 'test/init.js',
8 | 'src/*.js',
9 | 'test/spec/*.spec.js'
10 | ],
11 | frameworks: ['jasmine'],
12 | browsers: ['Chrome'],
13 | // coverage reporter generates the coverage
14 | reporters: ['progress', 'coverage'],
15 |
16 | port: 9876,
17 |
18 | preprocessors: {
19 | 'src/*.js': ['coverage']
20 | },
21 | coverageReporter: {
22 | type: 'html',
23 | dir: 'coverage/'
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/stConfig.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table')
2 | .constant('stConfig', {
3 | pagination: {
4 | template: 'template/smart-table/pagination.html',
5 | itemsByPage: 10,
6 | displayedPages: 5
7 | },
8 | search: {
9 | delay: 400, // ms
10 | inputEvent: 'input',
11 | trimSearch: false
12 | },
13 | select: {
14 | mode: 'single',
15 | selectedClass: 'st-selected'
16 | },
17 | sort: {
18 | ascentClass: 'st-sort-ascent',
19 | descentClass: 'st-sort-descent',
20 | descendingFirst: false,
21 | skipNatural: false,
22 | delay:300
23 | },
24 | pipe: {
25 | delay: 100 //ms
26 | }
27 | });
--------------------------------------------------------------------------------
/src/stSelectRow.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table')
2 | .directive('stSelectRow', ['stConfig', function (stConfig) {
3 | return {
4 | restrict: 'A',
5 | require: '^stTable',
6 | scope: {
7 | row: '=stSelectRow'
8 | },
9 | link: function (scope, element, attr, ctrl) {
10 | var mode = attr.stSelectMode || stConfig.select.mode;
11 | element.bind('click', function () {
12 | scope.$apply(function () {
13 | ctrl.select(scope.row, mode);
14 | });
15 | });
16 |
17 | scope.$watch('row.isSelected', function (newValue) {
18 | if (newValue === true) {
19 | element.addClass(stConfig.select.selectedClass);
20 | } else {
21 | element.removeClass(stConfig.select.selectedClass);
22 | }
23 | });
24 | }
25 | };
26 | }]);
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-smart-table",
3 | "version": "2.1.11",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "node ./node_modules/.bin/gulp karma-CI",
8 | "build": "node ./node_modules/.bin/gulp build"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/lorenzofox3/Smart-Table.git"
13 | },
14 | "author": "Laurent Renard",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "angular": "^1.6.4",
18 | "angular-mocks": "^1.6.4",
19 | "gulp": "^3.8.7",
20 | "gulp-concat": "^2.3.4",
21 | "gulp-insert": "^0.5.0",
22 | "gulp-sourcemaps": "^2.6.0",
23 | "gulp-uglify": "^3.0.0",
24 | "jasmine-core": "^2.6.3",
25 | "jquery": "^3.2.1",
26 | "karma": "^1.7.0",
27 | "karma-chrome-launcher": "^2.1.1",
28 | "karma-coverage": "^1.1.1",
29 | "karma-jasmine": "^1.1.0",
30 | "karma-phantomjs-launcher": "^1.0.4"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/stPipe.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table')
2 | .directive('stPipe', ['stConfig', '$timeout', function (config, $timeout) {
3 | return {
4 | require: 'stTable',
5 | scope: {
6 | stPipe: '='
7 | },
8 | link: {
9 |
10 | pre: function (scope, element, attrs, ctrl) {
11 |
12 | var pipePromise = null;
13 |
14 | if (ng.isFunction(scope.stPipe)) {
15 | ctrl.preventPipeOnWatch();
16 | ctrl.pipe = function () {
17 |
18 | if (pipePromise !== null) {
19 | $timeout.cancel(pipePromise)
20 | }
21 |
22 | pipePromise = $timeout(function () {
23 | scope.stPipe(ctrl.tableState(), ctrl);
24 | }, config.pipe.delay);
25 |
26 | return pipePromise;
27 | }
28 | }
29 | },
30 |
31 | post: function (scope, element, attrs, ctrl) {
32 | ctrl.pipe();
33 | }
34 | }
35 | };
36 | }]);
37 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Hello contributor !
2 |
3 | Please before you submit your issue, make sure all the below can be checked (note if anything is expected, you can simply add it under the relevant checkbox)
4 |
5 | - [ ] I have searched through the issue section and on [stackoverflow](http://stackoverflow.com/questions/tagged/smart-table?sort=newest&pageSize=30) if my issue has already been raised.
6 | - [ ] I have provided Angular version.
7 | - [ ] I have provided Smart table version.
8 | - [ ] I have set a precise description of my problem, mentioning the expected result.
9 | - [ ] I have given a way to reproduce my issue, by providing a running example , I can use [plunkr](http://plnkr.co/). Note if you want to mimic ajax loading behaviour you can use [$timeout](https://docs.angularjs.org/api/ng/service/$timeout) angular service or [$httpBackend](https://docs.angularjs.org/api/ng/service/$httpBackend).
10 |
11 | Note that it not all the above checkbox can be marked as checked the issue will immediately be closed. Thanks for your understanding.
12 |
13 | And don't forget to close you issue when it is solved !
14 |
15 | Thanks again for your contribution.
16 |
--------------------------------------------------------------------------------
/gulpFile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var concat = require('gulp-concat');
3 | var uglify = require('gulp-uglify');
4 | var karma = require('karma').server;
5 | var insert = require('gulp-insert');
6 | var sourcemaps = require('gulp-sourcemaps');
7 | var packageJson = require('./package.json');
8 | var pluginList = ['stSearch', 'stSelectRow', 'stSort', 'stPagination', 'stPipe'];
9 | var disFolder = './dist/';
10 | var src = (['smart-table.module', 'stConfig', 'stTable']).concat(pluginList).map(function (val) {
11 | return 'src/' + val + '.js';
12 | });
13 |
14 | src.push('src/bottom.txt');
15 | src.unshift('src/top.txt');
16 |
17 |
18 | gulp.task('karma-CI', function (done) {
19 | var conf = require('./test/karma.common.js');
20 | conf.singleRun = true;
21 | conf.browsers = ['PhantomJS'];
22 | conf.basePath = './';
23 | karma.start(conf, done);
24 | });
25 |
26 | gulp.task('uglify', function () {
27 | gulp.src(src)
28 | .pipe(concat('smart-table.min.js'))
29 | .pipe(sourcemaps.init())
30 | .pipe(uglify())
31 | .pipe(sourcemaps.write('.'))
32 | .pipe(gulp.dest(disFolder));
33 | });
34 |
35 | gulp.task('concat', function () {
36 | gulp.src(src, { base: '.' })
37 | .pipe(concat('smart-table.js'))
38 | .pipe(gulp.dest(disFolder));
39 | });
40 |
41 | gulp.task('test', ['karma-CI']);
42 |
43 | gulp.task('build',['test', 'uglify', 'concat'], function () {
44 |
45 | var version = packageJson.version;
46 | var string = '/** \n* @version ' + version + '\n* @license MIT\n*/\n';
47 |
48 | gulp.src(disFolder + '*.js')
49 | .pipe(insert.prepend(string))
50 | .pipe(gulp.dest(disFolder));
51 | });
52 |
--------------------------------------------------------------------------------
/test/spec/stPipe.spec.js:
--------------------------------------------------------------------------------
1 | describe('stPipe directive', function () {
2 | var scope;
3 |
4 | var firstArg;
5 | var secondArg;
6 |
7 | beforeEach(module('smart-table'));
8 |
9 | beforeEach(inject(function ($rootScope) {
10 |
11 | firstArg = undefined;
12 | secondArg = undefined;
13 |
14 | scope = $rootScope;
15 | scope.rowCollection = [];
16 | scope.customPipe = function customPipe(tableState, ctrl) {
17 | firstArg = tableState;
18 | secondArg = ctrl;
19 | }
20 | }));
21 |
22 | it('should use the custom pipe function with the current table state as argument', inject(function ($timeout,$compile) {
23 | var element;
24 | var template = '
' +
25 | '' +
26 | 'firstname ' +
27 | 'lastname ' +
28 | 'age ' +
29 | ' ' +
30 | '
';
31 | spyOn(scope, 'customPipe').and.callThrough();
32 |
33 | element = $compile(template)(scope);
34 |
35 | var ths = element.find('th');
36 | angular.element(ths[0]).triggerHandler('click');
37 |
38 | expect(firstArg).toBe(undefined);
39 | expect(secondArg).toBe(undefined);
40 |
41 | $timeout.flush();
42 |
43 | expect(firstArg).toEqual({
44 | sort: {predicate: 'name', reverse: false}, search: {}, pagination: {start: 0, totalItemCount: 0}
45 | });
46 |
47 | expect(secondArg.tableState()).toEqual({
48 | sort: {predicate: 'name', reverse: false}, search: {}, pagination: {start: 0, totalItemCount: 0}
49 | });
50 | }));
51 | });
--------------------------------------------------------------------------------
/src/stSearch.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table')
2 | .directive('stSearch', ['stConfig', '$timeout','$parse', function (stConfig, $timeout, $parse) {
3 | return {
4 | require: '^stTable',
5 | link: function (scope, element, attr, ctrl) {
6 | var tableCtrl = ctrl;
7 | var promise = null;
8 | var throttle = attr.stDelay || stConfig.search.delay;
9 | var event = attr.stInputEvent || stConfig.search.inputEvent;
10 | var trimSearch = attr.trimSearch || stConfig.search.trimSearch;
11 |
12 | attr.$observe('stSearch', function (newValue, oldValue) {
13 | var input = element[0].value;
14 | if (newValue !== oldValue && input) {
15 | ctrl.tableState().search = {};
16 | input = ng.isString(input) && trimSearch ? input.trim() : input;
17 | tableCtrl.search(input, newValue);
18 | }
19 | });
20 |
21 | //table state -> view
22 | scope.$watch(function () {
23 | return ctrl.tableState().search;
24 | }, function (newValue, oldValue) {
25 | var predicateExpression = attr.stSearch || '$';
26 | if (newValue.predicateObject && $parse(predicateExpression)(newValue.predicateObject) !== element[0].value) {
27 | element[0].value = $parse(predicateExpression)(newValue.predicateObject) || '';
28 | }
29 | }, true);
30 |
31 | // view -> table state
32 | element.bind(event, function (evt) {
33 | evt = evt.originalEvent || evt;
34 | if (promise !== null) {
35 | $timeout.cancel(promise);
36 | }
37 |
38 | promise = $timeout(function () {
39 | var input = evt.target.value;
40 | input = ng.isString(input) && trimSearch ? input.trim() : input;
41 | tableCtrl.search(input, attr.stSearch || '');
42 | promise = null;
43 | }, throttle);
44 | });
45 | }
46 | };
47 | }]);
48 |
--------------------------------------------------------------------------------
/src/stPagination.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table')
2 | .directive('stPagination', ['stConfig', function (stConfig) {
3 | return {
4 | restrict: 'EA',
5 | require: '^stTable',
6 | scope: {
7 | stItemsByPage: '=?',
8 | stDisplayedPages: '=?',
9 | stPageChange: '&'
10 | },
11 | templateUrl: function (element, attrs) {
12 | if (attrs.stTemplate) {
13 | return attrs.stTemplate;
14 | }
15 | return stConfig.pagination.template;
16 | },
17 | link: function (scope, element, attrs, ctrl) {
18 |
19 | scope.stItemsByPage = scope.stItemsByPage ? +(scope.stItemsByPage) : stConfig.pagination.itemsByPage;
20 | scope.stDisplayedPages = scope.stDisplayedPages ? +(scope.stDisplayedPages) : stConfig.pagination.displayedPages;
21 |
22 | scope.currentPage = 1;
23 | scope.pages = [];
24 |
25 | function redraw () {
26 | var paginationState = ctrl.tableState().pagination;
27 | var start = 1;
28 | var end;
29 | var i;
30 | var prevPage = scope.currentPage;
31 | scope.totalItemCount = paginationState.totalItemCount;
32 | scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;
33 |
34 | start = Math.max(start, scope.currentPage - Math.abs(Math.floor(scope.stDisplayedPages / 2)));
35 | end = start + scope.stDisplayedPages;
36 |
37 | if (end > paginationState.numberOfPages) {
38 | end = paginationState.numberOfPages + 1;
39 | start = Math.max(1, end - scope.stDisplayedPages);
40 | }
41 |
42 | scope.pages = [];
43 | scope.numPages = paginationState.numberOfPages;
44 |
45 | for (i = start; i < end; i++) {
46 | scope.pages.push(i);
47 | }
48 |
49 | if (prevPage !== scope.currentPage) {
50 | scope.stPageChange({newPage: scope.currentPage});
51 | }
52 | }
53 |
54 | //table state --> view
55 | scope.$watch(function () {
56 | return ctrl.tableState().pagination;
57 | }, redraw, true);
58 |
59 | //scope --> table state (--> view)
60 | scope.$watch('stItemsByPage', function (newValue, oldValue) {
61 | if (newValue !== oldValue) {
62 | scope.selectPage(1);
63 | }
64 | });
65 |
66 | scope.$watch('stDisplayedPages', redraw);
67 |
68 | //view -> table state
69 | scope.selectPage = function (page) {
70 | if (page > 0 && page <= scope.numPages) {
71 | ctrl.slice((page - 1) * scope.stItemsByPage, scope.stItemsByPage);
72 | }
73 | };
74 |
75 | if (!ctrl.tableState().pagination.number) {
76 | ctrl.slice(0, scope.stItemsByPage);
77 | }
78 | }
79 | };
80 | }]);
81 |
--------------------------------------------------------------------------------
/src/stSort.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table')
2 | .directive('stSort', ['stConfig', '$parse', '$timeout', function (stConfig, $parse, $timeout) {
3 | return {
4 | restrict: 'A',
5 | require: '^stTable',
6 | link: function (scope, element, attr, ctrl) {
7 |
8 | var predicate = attr.stSort;
9 | var getter = $parse(predicate);
10 | var index = 0;
11 | var classAscent = attr.stClassAscent || stConfig.sort.ascentClass;
12 | var classDescent = attr.stClassDescent || stConfig.sort.descentClass;
13 | var stateClasses = [classAscent, classDescent];
14 | var sortDefault;
15 | var skipNatural = attr.stSkipNatural !== undefined ? attr.stSkipNatural : stConfig.sort.skipNatural;
16 | var descendingFirst = attr.stDescendingFirst !== undefined ? attr.stDescendingFirst : stConfig.sort.descendingFirst;
17 | var promise = null;
18 | var throttle = attr.stDelay || stConfig.sort.delay;
19 |
20 | // set aria attributes
21 | var ariaSort = 'aria-sort';
22 | var ariaSortNone = 'none';
23 | var ariaSortAscending = 'ascending';
24 | var ariaSortDescending = 'descending';
25 | element
26 | .attr('role', 'columnheader')
27 | .attr(ariaSort, ariaSortNone);
28 |
29 | if (attr.stSortDefault) {
30 | sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;
31 | }
32 |
33 | //view --> table state
34 | function sort () {
35 | if (descendingFirst) {
36 | index = index === 0 ? 2 : index - 1;
37 | } else {
38 | index++;
39 | }
40 |
41 | var func;
42 | predicate = ng.isFunction(getter(scope)) || ng.isArray(getter(scope)) ? getter(scope) : attr.stSort;
43 | if (index % 3 === 0 && !!skipNatural !== true) {
44 | //manual reset
45 | index = 0;
46 | ctrl.tableState().sort = {};
47 | ctrl.tableState().pagination.start = 0;
48 | func = ctrl.pipe.bind(ctrl);
49 | } else {
50 | func = ctrl.sortBy.bind(ctrl, predicate, index % 2 === 0);
51 | }
52 | if (promise !== null) {
53 | $timeout.cancel(promise);
54 | }
55 | if (throttle < 0) {
56 | func();
57 | } else {
58 | promise = $timeout(function(){
59 | func();
60 | }, throttle);
61 | }
62 | }
63 |
64 | element.bind('click', function sortClick () {
65 | if (predicate) {
66 | scope.$apply(sort);
67 | }
68 | });
69 |
70 | if (sortDefault) {
71 | index = sortDefault === 'reverse' ? 1 : 0;
72 | sort();
73 | }
74 |
75 | //table state --> view
76 | scope.$watch(function () {
77 | return ctrl.tableState().sort;
78 | }, function (newValue) {
79 | if (newValue.predicate !== predicate) {
80 | index = 0;
81 | element
82 | .removeClass(classAscent)
83 | .removeClass(classDescent)
84 | .attr(ariaSort, ariaSortNone);
85 | } else {
86 | index = newValue.reverse === true ? 2 : 1;
87 | element
88 | .removeClass(stateClasses[index % 2])
89 | .addClass(stateClasses[index - 1])
90 | .attr(ariaSort, newValue.reverse ? ariaSortAscending : ariaSortDescending);
91 | }
92 | }, true);
93 | }
94 | };
95 | }]);
96 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/lorenzofox3/Smart-Table)
2 |
3 | **Hey I am not too much into angular these days. If someone wants to maintain this project, please contact me !**
4 |
5 | # Smart Table
6 |
7 | Smart Table is a table module for angular js. It allows you to quickly compose your table in a declarative way including sorting, filtering, row selection, and pagination.
8 | It is lightweight (around 3kb minified) and has no other dependencies than Angular itself.
9 | Check the [documentation](http://lorenzofox3.github.io/smart-table-website/) website for more details
10 |
11 | ## Submitting an issue
12 |
13 | Please be responsible -- investigate potential issues yourself to eliminate the possibility that your issue isn't just an error. If you are still having problems, try posting on our [gitter](https://gitter.im/lorenzofox3/Smart-Table). When submitting an issue try as much as possible to:
14 |
15 | 1. Search in the already existing issues or on [stackoverflow](http://stackoverflow.com/questions/tagged/smart-table?sort=newest&pageSize=30) if your issue has not been raised before.
16 |
17 | 2. Give a precise description mentionning angular version, smart-table version.
18 |
19 | 3. Give a way to reproduce your issue, the best would be with a running example , you can use [plunkr](http://plnkr.co/) (smart-table is the list of available packages). Note if you want to mimic ajax loading behaviour you can use [$timeout](https://docs.angularjs.org/api/ng/service/$timeout) angular service or [$httpBackend](https://docs.angularjs.org/api/ng/service/$httpBackend).
20 |
21 | 4. Isolate your code sample on the probable issue to avoid pollution and noise.
22 |
23 | 5. Close your issue when a solution has been found (and share it with the community).
24 |
25 | Note that 80% of the open issues are actually not issues but due to lack of good investigation. These issues create unnecessary work, so please be considerate.
26 |
27 | Any open issue which do not follow the steps above will be closed without investigation.
28 |
29 | ## Install
30 |
31 | The easiest way is to run `bower install angular-smart-table`, then you just have to add the script and register the module `smart-table` to you application.
32 |
33 | You can also install using NPM `npm install angular-smart-table`, so you can use with browserify or webpack
34 |
35 | ## Test
36 |
37 | Run `npm install` after you have installed the dependencies (`npm install` and `bower install`).
38 |
39 | ## Custom builds
40 |
41 | Smart Table is based around a main directive which generate a top level controller whose API can be accessed by sub directives
42 | (plugins). If you don't need some of these, simply edit the gulpfile (the pluginList variable) and run `gulp build`.
43 |
44 | ## Older versions
45 |
46 | Smart Table used to be configuration based and if you rely on this version, you can still access the code on the [0.2.x](https://github.com/lorenzofox3/Smart-Table/tree/vx.2.x) branch. You will be able to find the documentation related to this version
47 | [here](https://github.com/lorenzofox3/smart-table-website) (simply open index.html in a browser).
48 |
49 | Note, I have closed all the issues related to these versions as people get confused when reading these issues and commented on them like it was related to the newer version. Feel free to reopen any of them (or open a new one), but don't forget to mention it is related to the older versions.
50 |
51 | ## License
52 |
53 | Smart Table module is under MIT license:
54 |
55 | > Copyright (C) 2016 Laurent Renard.
56 | >
57 | > Permission is hereby granted, free of charge, to any person
58 | > obtaining a copy of this software and associated documentation files
59 | > (the "Software"), to deal in the Software without restriction,
60 | > including without limitation the rights to use, copy, modify, merge,
61 | > publish, distribute, sublicense, and/or sell copies of the Software,
62 | > and to permit persons to whom the Software is furnished to do so,
63 | > subject to the following conditions:
64 | >
65 | > The above copyright notice and this permission notice shall be
66 | > included in all copies or substantial portions of the Software.
67 | >
68 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
69 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
70 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
71 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
72 | > BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
73 | > ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
74 | > CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
75 | > SOFTWARE.
76 |
--------------------------------------------------------------------------------
/changeLog.md:
--------------------------------------------------------------------------------
1 | ## version 1.1.0
2 |
3 | * allow binding on search predicate ([#142](https://github.com/lorenzofox3/Smart-Table/issues/142)).
4 | Note that if you want to search against a property name you have now to put in under single quote otherwise it will be considered as a binding
5 | ```markup
6 |
7 | ```
8 |
9 | ## version 1.1.1
10 |
11 | * fix [#146](https://github.com/lorenzofox3/Smart-Table/issues/146) and [#148](https://github.com/lorenzofox3/Smart-Table/issues/148), set stPipe before stPagination is called. Thanks [brianchance](https://github.com/brianchance)
12 |
13 | ## version 1.2.1
14 |
15 | * implement [#149](https://github.com/lorenzofox3/Smart-Table/issues/149) (default sorting)
16 |
17 | ## version 1.2.2
18 |
19 | * hide pagination when less than 1 page
20 | * add unit tests for pagination directive
21 |
22 | ## version 1.2.3
23 |
24 | * fix back to natural sort order
25 | * use same strategy view -> table state, table state -> view for all the plugins
26 |
27 | ## version 1.2.4
28 |
29 | * fix [#161](https://github.com/lorenzofox3/Smart-Table/issues/161)
30 |
31 | ## version 1.2.5
32 |
33 | * fix [#162](https://github.com/lorenzofox3/Smart-Table/issues/162)
34 |
35 | ## version 1.2.6
36 |
37 | * fix [#165](https://github.com/lorenzofox3/Smart-Table/issues/165)
38 | * ability to overwrite class names for (st-sort-ascent and st-sort-descent) thanks to [replacement87](https://github.com/replacement87)
39 |
40 | ## version 1.2.7
41 |
42 | * fix [#167](https://github.com/lorenzofox3/Smart-Table/issues/167)
43 |
44 | ## version 1.3.0
45 |
46 | * new feature, items by page and displayed page can be bound
47 |
48 | ## version 1.4.0
49 |
50 | * support external template for pagination
51 | * support angular v1.3.x
52 |
53 | ## version 1.4.1
54 |
55 | * ability to skip natural ordering state (ie fix [#192](https://github.com/lorenzofox3/Smart-Table/issues/192))
56 |
57 | ## versiokn 1.4.2
58 |
59 | * fix [#200](https://github.com/lorenzofox3/Smart-Table/issues/200), `this` in a custom pipe function does not refer to the table controller anymore, and the signature of a custom pipe function is
60 | ```javascript
61 | function(tableState, tableController){
62 |
63 | }
64 | ```
65 |
66 | ## version 1.4.3
67 |
68 | * ability to set filter function st-set-filter
69 | * ability to set sort function st-set-sort
70 |
71 | ## version 1.4.4
72 |
73 | * patch for sync problem
74 |
75 | ## version 1.4.5
76 |
77 | * merge [#234](https://github.com/lorenzofox3/Smart-Table/issues/234), [#218](https://github.com/lorenzofox3/Smart-Table/issues/218)
78 | * fix [#233](https://github.com/lorenzofox3/Smart-Table/issues/2332), [#237](https://github.com/lorenzofox3/Smart-Table/issues/237)
79 |
80 | ## version 1.4.6
81 |
82 | * evaluate sort predicate as late as possible
83 | * fix [#262](https://github.com/lorenzofox3/Smart-Table/issues/262)
84 |
85 | ## version 1.4.7
86 |
87 | * fix [#276](https://github.com/lorenzofox3/Smart-Table/issues/276)
88 |
89 | ## version 1.4.8
90 |
91 | * fix [#281](https://github.com/lorenzofox3/Smart-Table/issues/281)
92 |
93 | ## version 1.4.9
94 |
95 | * fix [#285](https://github.com/lorenzofox3/Smart-Table/issues/285)
96 |
97 | ## version 1.4.10
98 |
99 | * fix [#284](https://github.com/lorenzofox3/Smart-Table/issues/284)
100 | * fix [#290](https://github.com/lorenzofox3/Smart-Table/issues/290)
101 |
102 | ## version 1.4.11
103 |
104 | * fix [#296](https://github.com/lorenzofox3/Smart-Table/issues/296)
105 | * add possibility to bind a callback when page changes
106 |
107 | ## version 1.4.12
108 |
109 | * don't use pagination class twice
110 | * build improvement
111 |
112 | ## version 1.4.13
113 |
114 | * use a global configuration
115 | * expose filtered collection result
116 |
117 | ## version 2.0.0
118 |
119 | * use interpolation rather than binding for st-search directive (to avoid the creation of isolated scope)
120 |
121 | **This is a breaking change as now, you will have to remove the single quote around the predicate property name, and if you were using a binding, you'll have to interpolate it with the curly brace notation**
122 |
123 | ## version 2.0.1
124 |
125 | * fix [#328](https://github.com/lorenzofox3/Smart-Table/issues/328)
126 |
127 | ## version 2.0.2
128 |
129 | * add debounce to custom pipe function to make sure tableState is stable
130 | * fix [#329](https://github.com/lorenzofox3/Smart-Table/issues/329)
131 |
132 | ## version 2.0.3
133 |
134 | * implements [#379](https://github.com/lorenzofox3/Smart-Table/issues/379)
135 | * fix [#390](https://github.com/lorenzofox3/Smart-Table/issues/390)
136 |
137 | ## version 2.1.0
138 |
139 | * support nested search (thanks to @jansabbe)
140 | * fix [#254](https://github.com/lorenzofox3/Smart-Table/issues/254)
141 | * fix wrong path to default config for stSkipNatural (@phuvo)
142 | * fix [#406](https://github.com/lorenzofox3/Smart-Table/issues/406)
143 |
144 | ## version 2.1.1
145 |
146 | * support commonjs
147 | * add totalItemCount on tableState (@eirikbell)
148 |
149 | ## version 2.1.2
150 |
151 | * improve build [#461](https://github.com/lorenzofox3/Smart-Table/issues/461) [stanleyxu](https://github.com/stanleyxu2005)
152 |
153 | ## version 2.1.3
154 |
155 | * fix [#477](https://github.com/lorenzofox3/Smart-Table/issues/477)
156 |
157 | ## version 2.1.4
158 |
159 | * add throttle to sort
160 | * add watch to first item in collection (@matthewbednarski)
161 |
162 | ## version 2.1.5
163 |
164 | * added multiple sort support to st-sort, [#544](https://github.com/lorenzofox3/Smart-Table/issues/544)
165 | * fix [#533](https://github.com/lorenzofox3/Smart-Table/issues/533)
166 | * fix [#515](https://github.com/lorenzofox3/Smart-Table/issues/515)
167 |
168 | ## version 2.1.6
169 |
170 | * fix [#559](https://github.com/lorenzofox3/Smart-Table/issues/559)
171 |
172 | ## version 2.1.7
173 |
174 | * fix [#468](https://github.com/lorenzofox3/Smart-Table/issues/468) thanks to Douglas-Treadwell
175 |
176 |
--------------------------------------------------------------------------------
/dist/smart-table.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @version 2.1.11
3 | * @license MIT
4 | */
5 | !function(t,e){"use strict";t.module("smart-table",[]).run(["$templateCache",function(t){t.put("template/smart-table/pagination.html",' ')}]),t.module("smart-table").constant("stConfig",{pagination:{template:"template/smart-table/pagination.html",itemsByPage:10,displayedPages:5},search:{delay:400,inputEvent:"input",trimSearch:!1},select:{mode:"single",selectedClass:"st-selected"},sort:{ascentClass:"st-sort-ascent",descentClass:"st-sort-descent",descendingFirst:!1,skipNatural:!1,delay:300},pipe:{delay:100}}),t.module("smart-table").controller("stTableController",["$scope","$parse","$filter","$attrs",function(a,n,s,r){function i(t){return t?[].concat(t):[]}function c(){b=i(o(a)),!0===S&&P.pipe()}function l(t,e){if(-1!=e.indexOf(".")){var a=e.split("."),s=a.pop(),r=a.join("."),i=n(r)(t);delete i[s],0==Object.keys(i).length&&l(t,r)}else delete t[e]}var o,u,p,g=r.stTable,d=n(g),f=d.assign,m=s("orderBy"),h=s("filter"),b=i(d(a)),v={sort:{},search:{},pagination:{start:0,totalItemCount:0}},S=!0,P=this;r.stSafeSrc&&(o=n(r.stSafeSrc),a.$watch(function(){var t=o(a);return t&&t.length?t[0]:e},function(t,e){t!==e&&c()}),a.$watch(function(){var t=o(a);return t?t.length:0},function(t,e){t!==b.length&&c()}),a.$watch(function(){return o(a)},function(t,e){t!==e&&(v.pagination.start=0,c())})),this.sortBy=function(e,a){return v.sort.predicate=e,v.sort.reverse=!0===a,t.isFunction(e)?v.sort.functionName=e.name:delete v.sort.functionName,v.pagination.start=0,this.pipe()},this.search=function(t,e,a){var s=v.search.predicateObject||{},r=e||"$";return n(r).assign(s,t),t||l(s,r),v.search.predicateObject=s,v.pagination.start=0,this.pipe()},this.pipe=function(){var t,n=v.pagination;u=v.search.predicateObject?h(b,v.search.predicateObject):b,v.sort.predicate&&(u=m(u,v.sort.predicate,v.sort.reverse)),n.totalItemCount=u.length,n.number!==e&&(n.numberOfPages=u.length>0?Math.ceil(u.length/n.number):1,n.start=n.start>=u.length?(n.numberOfPages-1)*n.number:n.start,t=u.slice(n.start,n.start+parseInt(n.number))),f(a,t||u)},this.select=function(t,n){var s=i(d(a)),r=s.indexOf(t);-1!==r&&("single"===n?(t.isSelected=!0!==t.isSelected,p&&(p.isSelected=!1),p=!0===t.isSelected?t:e):s[r].isSelected=!s[r].isSelected)},this.slice=function(t,e){return v.pagination.start=t,v.pagination.number=e,this.pipe()},this.tableState=function(){return v},this.getFilteredCollection=function(){return u||b},this.setFilterFunction=function(t){h=s(t)},this.setSortFunction=function(t){m=s(t)},this.preventPipeOnWatch=function(){S=!1}}]).directive("stTable",function(){return{restrict:"A",controller:"stTableController",link:function(t,e,a,n){a.stSetFilter&&n.setFilterFunction(a.stSetFilter),a.stSetSort&&n.setSortFunction(a.stSetSort)}}}),t.module("smart-table").directive("stSearch",["stConfig","$timeout","$parse",function(e,a,n){return{require:"^stTable",link:function(s,r,i,c){var l=c,o=null,u=i.stDelay||e.search.delay,p=i.stInputEvent||e.search.inputEvent,g=i.trimSearch||e.search.trimSearch;i.$observe("stSearch",function(e,a){var n=r[0].value;e!==a&&n&&(c.tableState().search={},n=t.isString(n)&&g?n.trim():n,l.search(n,e))}),s.$watch(function(){return c.tableState().search},function(t,e){var a=i.stSearch||"$";t.predicateObject&&n(a)(t.predicateObject)!==r[0].value&&(r[0].value=n(a)(t.predicateObject)||"")},!0),r.bind(p,function(e){e=e.originalEvent||e,null!==o&&a.cancel(o),o=a(function(){var a=e.target.value;a=t.isString(a)&&g?a.trim():a,l.search(a,i.stSearch||""),o=null},u)})}}}]),t.module("smart-table").directive("stSelectRow",["stConfig",function(t){return{restrict:"A",require:"^stTable",scope:{row:"=stSelectRow"},link:function(e,a,n,s){var r=n.stSelectMode||t.select.mode;a.bind("click",function(){e.$apply(function(){s.select(e.row,r)})}),e.$watch("row.isSelected",function(e){!0===e?a.addClass(t.select.selectedClass):a.removeClass(t.select.selectedClass)})}}}]),t.module("smart-table").directive("stSort",["stConfig","$parse","$timeout",function(a,n,s){return{restrict:"A",require:"^stTable",link:function(r,i,c,l){function o(){v?d=0===d?2:d-1:d++;var e;p=t.isFunction(g(r))||t.isArray(g(r))?g(r):c.stSort,d%3==0&&!0!=!!b?(d=0,l.tableState().sort={},l.tableState().pagination.start=0,e=l.pipe.bind(l)):e=l.sortBy.bind(l,p,d%2==0),null!==S&&s.cancel(S),P<0?e():S=s(function(){e()},P)}var u,p=c.stSort,g=n(p),d=0,f=c.stClassAscent||a.sort.ascentClass,m=c.stClassDescent||a.sort.descentClass,h=[f,m],b=c.stSkipNatural!==e?c.stSkipNatural:a.sort.skipNatural,v=c.stDescendingFirst!==e?c.stDescendingFirst:a.sort.descendingFirst,S=null,P=c.stDelay||a.sort.delay,y="aria-sort";i.attr("role","columnheader").attr(y,"none"),c.stSortDefault&&(u=r.$eval(c.stSortDefault)!==e?r.$eval(c.stSortDefault):c.stSortDefault),i.bind("click",function(){p&&r.$apply(o)}),u&&(d="reverse"===u?1:0,o()),r.$watch(function(){return l.tableState().sort},function(t){t.predicate!==p?(d=0,i.removeClass(f).removeClass(m).attr(y,"none")):(d=!0===t.reverse?2:1,i.removeClass(h[d%2]).addClass(h[d-1]).attr(y,t.reverse?"ascending":"descending"))},!0)}}}]),t.module("smart-table").directive("stPagination",["stConfig",function(t){return{restrict:"EA",require:"^stTable",scope:{stItemsByPage:"=?",stDisplayedPages:"=?",stPageChange:"&"},templateUrl:function(e,a){return a.stTemplate?a.stTemplate:t.pagination.template},link:function(e,a,n,s){function r(){var t,a,n=s.tableState().pagination,r=1,i=e.currentPage;for(e.totalItemCount=n.totalItemCount,e.currentPage=Math.floor(n.start/n.number)+1,(t=(r=Math.max(r,e.currentPage-Math.abs(Math.floor(e.stDisplayedPages/2))))+e.stDisplayedPages)>n.numberOfPages&&(t=n.numberOfPages+1,r=Math.max(1,t-e.stDisplayedPages)),e.pages=[],e.numPages=n.numberOfPages,a=r;a0&&t<=e.numPages&&s.slice((t-1)*e.stItemsByPage,e.stItemsByPage)},s.tableState().pagination.number||s.slice(0,e.stItemsByPage)}}}]),t.module("smart-table").directive("stPipe",["stConfig","$timeout",function(e,a){return{require:"stTable",scope:{stPipe:"="},link:{pre:function(n,s,r,i){var c=null;t.isFunction(n.stPipe)&&(i.preventPipeOnWatch(),i.pipe=function(){return null!==c&&a.cancel(c),c=a(function(){n.stPipe(i.tableState(),i)},e.pipe.delay)})},post:function(t,e,a,n){n.pipe()}}}}])}(angular);
6 | //# sourceMappingURL=smart-table.min.js.map
7 |
--------------------------------------------------------------------------------
/test/spec/stSelectRow.spec.js:
--------------------------------------------------------------------------------
1 | describe('stSelectRow Directive', function () {
2 |
3 | var rootScope;
4 | var scope;
5 | var element;
6 |
7 | function hasClass(element, classname) {
8 | return Array.prototype.indexOf.call(element.classList, classname) !== -1
9 | }
10 |
11 | beforeEach(module('smart-table'));
12 |
13 | var stConfig;
14 | beforeEach(inject(function (_stConfig_) {
15 | stConfig = _stConfig_;
16 | }));
17 |
18 | // We need to do this here because we must compile the directive *after* configuring stConfig
19 | it('should select more than one row when globally configured', inject(function ($rootScope, $compile) {
20 | var oldMode = stConfig.select.mode;
21 | stConfig.select.mode = 'multiple';
22 |
23 |
24 | rootScope = $rootScope;
25 | scope = $rootScope.$new();
26 | rootScope.rowCollection = [
27 | {name: 'Renard', firstname: 'Laurent', age: 66},
28 | {name: 'Francoise', firstname: 'Frere', age: 99},
29 | {name: 'Renard', firstname: 'Olivier', age: 33},
30 | {name: 'Leponge', firstname: 'Bob', age: 22},
31 | {name: 'Faivre', firstname: 'Blandine', age: 44}
32 | ];
33 |
34 | var template = '' +
35 | '' +
36 | ' ' +
37 | ' ' +
38 | '
';
39 |
40 | element = $compile(template)(scope);
41 |
42 | scope.$apply();
43 |
44 |
45 | var trs = element.find('tr');
46 | expect(trs.length).toBe(5);
47 | angular.element(trs[3]).triggerHandler('click');
48 | expect(scope.rowCollection[3].isSelected).toBe(true);
49 | angular.element(trs[1]).triggerHandler('click');
50 | expect(scope.rowCollection[1].isSelected).toBe(true);
51 | expect(scope.rowCollection[3].isSelected).toBe(true);
52 |
53 | stConfig.select.mode = oldMode;
54 | }));
55 |
56 | describe('single mode', function () {
57 | beforeEach(inject(function ($compile, $rootScope) {
58 |
59 | rootScope = $rootScope;
60 | scope = $rootScope.$new();
61 | rootScope.rowCollection = [
62 | {name: 'Renard', firstname: 'Laurent', age: 66},
63 | {name: 'Francoise', firstname: 'Frere', age: 99},
64 | {name: 'Renard', firstname: 'Olivier', age: 33},
65 | {name: 'Leponge', firstname: 'Bob', age: 22},
66 | {name: 'Faivre', firstname: 'Blandine', age: 44}
67 | ];
68 |
69 | var template = '' +
70 | '' +
71 | ' ' +
72 | ' ' +
73 | '
';
74 |
75 | element = $compile(template)(scope);
76 |
77 | scope.$apply();
78 | }));
79 |
80 | it('should select one row', function () {
81 | var trs = element.find('tr');
82 | expect(trs.length).toBe(5);
83 | angular.element(trs[3]).triggerHandler('click');
84 | expect(scope.rowCollection[3].isSelected).toBe(true);
85 | });
86 |
87 | it('should select one row only by default', function () {
88 | var trs = element.find('tr');
89 | expect(trs.length).toBe(5);
90 | angular.element(trs[3]).triggerHandler('click');
91 | expect(scope.rowCollection[3].isSelected).toBe(true);
92 | angular.element(trs[1]).triggerHandler('click');
93 | expect(scope.rowCollection[1].isSelected).toBe(true);
94 | expect(scope.rowCollection[3].isSelected).toBe(false);
95 | });
96 |
97 | it('should update the class name when isSelected property change', function () {
98 |
99 | var tr = element.find('tr');
100 | expect(hasClass(tr[2], 'st-selected')).toBe(false);
101 | scope.rowCollection[2].isSelected = true;
102 | scope.$apply();
103 | expect(hasClass(tr[2], 'st-selected')).toBe(true);
104 |
105 | scope.rowCollection[2].isSelected = false;
106 | scope.$apply();
107 | expect(hasClass(tr[2], 'st-selected')).toBe(false);
108 | });
109 |
110 | it('should update the customized class name when isSelected property change', function () {
111 | var oldClass = stConfig.select.selectedClass;
112 | stConfig.select.selectedClass = 'custom-selected'
113 |
114 | var tr = element.find('tr');
115 | expect(hasClass(tr[2], 'custom-selected')).toBe(false);
116 | scope.rowCollection[2].isSelected = true;
117 | scope.$apply();
118 | expect(hasClass(tr[2], 'custom-selected')).toBe(true);
119 |
120 | scope.rowCollection[2].isSelected = false;
121 | scope.$apply();
122 | expect(hasClass(tr[2], 'custom-selected')).toBe(false);
123 |
124 | stConfig.select.selectedClass = oldClass;
125 | });
126 | });
127 |
128 | describe('multiple mode', function () {
129 | beforeEach(inject(function ($compile, $rootScope) {
130 |
131 | rootScope = $rootScope;
132 | scope = $rootScope.$new();
133 | scope.rowCollection = [
134 | {name: 'Renard', firstname: 'Laurent', age: 66},
135 | {name: 'Francoise', firstname: 'Frere', age: 99},
136 | {name: 'Renard', firstname: 'Olivier', age: 33},
137 | {name: 'Leponge', firstname: 'Bob', age: 22},
138 | {name: 'Faivre', firstname: 'Blandine', age: 44}
139 | ];
140 |
141 | var template = '' +
142 | '' +
143 | ' ' +
144 | ' ' +
145 | '
';
146 |
147 | element = $compile(template)(scope);
148 |
149 | scope.$apply();
150 | }));
151 |
152 | it('should select multiple row', function () {
153 | var trs = element.find('tr');
154 | expect(trs.length).toBe(5);
155 | angular.element(trs[3]).triggerHandler('click');
156 | expect(scope.rowCollection[3].isSelected).toBe(true);
157 | angular.element(trs[1]).triggerHandler('click');
158 | expect(scope.rowCollection[3].isSelected).toBe(true);
159 | expect(scope.rowCollection[1].isSelected).toBe(true);
160 | });
161 | });
162 |
163 |
164 | });
165 |
--------------------------------------------------------------------------------
/src/stTable.js:
--------------------------------------------------------------------------------
1 | ng.module('smart-table').controller('stTableController', [
2 | '$scope',
3 | '$parse',
4 | '$filter',
5 | '$attrs',
6 | function StTableController($scope, $parse, $filter, $attrs) {
7 | var propertyName = $attrs.stTable;
8 | var displayGetter = $parse(propertyName);
9 | var displaySetter = displayGetter.assign;
10 | var safeGetter;
11 | var orderBy = $filter('orderBy');
12 | var filter = $filter('filter');
13 | var safeCopy = copyRefs(displayGetter($scope));
14 | var tableState = {
15 | sort: {},
16 | search: {},
17 | pagination: { start: 0, totalItemCount: 0 }
18 | };
19 | var filtered;
20 | var pipeAfterSafeCopy = true;
21 | var ctrl = this;
22 | var lastSelected;
23 |
24 | function copyRefs(src) {
25 | return src ? [].concat(src) : [];
26 | }
27 |
28 | function updateSafeCopy() {
29 | safeCopy = copyRefs(safeGetter($scope));
30 | if (pipeAfterSafeCopy === true) {
31 | ctrl.pipe();
32 | }
33 | }
34 |
35 | function deepDelete(object, path) {
36 | if (path.indexOf('.') != -1) {
37 | var partials = path.split('.');
38 | var key = partials.pop();
39 | var parentPath = partials.join('.');
40 | var parentObject = $parse(parentPath)(object);
41 | delete parentObject[key];
42 | if (Object.keys(parentObject).length == 0) {
43 | deepDelete(object, parentPath);
44 | }
45 | } else {
46 | delete object[path];
47 | }
48 | }
49 |
50 | if ($attrs.stSafeSrc) {
51 | safeGetter = $parse($attrs.stSafeSrc);
52 | $scope.$watch(
53 | function() {
54 | var safeSrc = safeGetter($scope);
55 | return safeSrc && safeSrc.length ? safeSrc[0] : undefined;
56 | },
57 | function(newValue, oldValue) {
58 | if (newValue !== oldValue) {
59 | updateSafeCopy();
60 | }
61 | }
62 | );
63 | $scope.$watch(
64 | function() {
65 | var safeSrc = safeGetter($scope);
66 | return safeSrc ? safeSrc.length : 0;
67 | },
68 | function(newValue, oldValue) {
69 | if (newValue !== safeCopy.length) {
70 | updateSafeCopy();
71 | }
72 | }
73 | );
74 | $scope.$watch(
75 | function() {
76 | return safeGetter($scope);
77 | },
78 | function(newValue, oldValue) {
79 | if (newValue !== oldValue) {
80 | tableState.pagination.start = 0;
81 | updateSafeCopy();
82 | }
83 | }
84 | );
85 | }
86 |
87 | /**
88 | * sort the rows
89 | * @param {Function | String} predicate - function or string which will be used as predicate for the sorting
90 | * @param [reverse] - if you want to reverse the order
91 | */
92 | this.sortBy = function sortBy(predicate, reverse) {
93 | tableState.sort.predicate = predicate;
94 | tableState.sort.reverse = reverse === true;
95 |
96 | if (ng.isFunction(predicate)) {
97 | tableState.sort.functionName = predicate.name;
98 | } else {
99 | delete tableState.sort.functionName;
100 | }
101 |
102 | tableState.pagination.start = 0;
103 | return this.pipe();
104 | };
105 |
106 | /**
107 | * search matching rows
108 | * @param {String} input - the input string
109 | * @param {String} [predicate] - the property name against you want to check the match, otherwise it will search on all properties
110 | * @param {String | Function } [comparator] - a comparator to pass to the filter for the (pass true for stric mode)
111 | */
112 | this.search = function search(input, predicate, comparator) {
113 | var predicateObject = tableState.search.predicateObject || {};
114 | var prop = predicate ? predicate : '$';
115 |
116 | $parse(prop).assign(predicateObject, input);
117 | // to avoid to filter out null value
118 | if (!input) {
119 | deepDelete(predicateObject, prop);
120 | }
121 | tableState.search.predicateObject = predicateObject;
122 | tableState.pagination.start = 0;
123 | return this.pipe();
124 | };
125 |
126 | /**
127 | * this will chain the operations of sorting and filtering based on the current table state (sort options, filtering, ect)
128 | */
129 | this.pipe = function pipe() {
130 | var pagination = tableState.pagination;
131 | var output;
132 | filtered = tableState.search.predicateObject
133 | ? filter(safeCopy, tableState.search.predicateObject)
134 | : safeCopy;
135 | if (tableState.sort.predicate) {
136 | filtered = orderBy(
137 | filtered,
138 | tableState.sort.predicate,
139 | tableState.sort.reverse
140 | );
141 | }
142 | pagination.totalItemCount = filtered.length;
143 | if (pagination.number !== undefined) {
144 | pagination.numberOfPages = filtered.length > 0
145 | ? Math.ceil(filtered.length / pagination.number)
146 | : 1;
147 | pagination.start = pagination.start >= filtered.length
148 | ? (pagination.numberOfPages - 1) * pagination.number
149 | : pagination.start;
150 | output = filtered.slice(
151 | pagination.start,
152 | pagination.start + parseInt(pagination.number)
153 | );
154 | }
155 | displaySetter($scope, output || filtered);
156 | };
157 |
158 | /**
159 | * select a dataRow (it will add the attribute isSelected to the row object)
160 | * @param {Object} row - the row to select
161 | * @param {String} [mode] - "single" or "multiple" (multiple by default)
162 | */
163 | this.select = function select(row, mode) {
164 | var rows = copyRefs(displayGetter($scope));
165 | var index = rows.indexOf(row);
166 | if (index !== -1) {
167 | if (mode === 'single') {
168 | row.isSelected = row.isSelected !== true;
169 | if (lastSelected) {
170 | lastSelected.isSelected = false;
171 | }
172 | lastSelected = row.isSelected === true ? row : undefined;
173 | } else {
174 | rows[index].isSelected = !rows[index].isSelected;
175 | }
176 | }
177 | };
178 |
179 | /**
180 | * take a slice of the current sorted/filtered collection (pagination)
181 | *
182 | * @param {Number} start - start index of the slice
183 | * @param {Number} number - the number of item in the slice
184 | */
185 | this.slice = function splice(start, number) {
186 | tableState.pagination.start = start;
187 | tableState.pagination.number = number;
188 | return this.pipe();
189 | };
190 |
191 | /**
192 | * return the current state of the table
193 | * @returns {{sort: {}, search: {}, pagination: {start: number}}}
194 | */
195 | this.tableState = function getTableState() {
196 | return tableState;
197 | };
198 |
199 | this.getFilteredCollection = function getFilteredCollection() {
200 | return filtered || safeCopy;
201 | };
202 |
203 | /**
204 | * Use a different filter function than the angular FilterFilter
205 | * @param filterName the name under which the custom filter is registered
206 | */
207 | this.setFilterFunction = function setFilterFunction(filterName) {
208 | filter = $filter(filterName);
209 | };
210 |
211 | /**
212 | * Use a different function than the angular orderBy
213 | * @param sortFunctionName the name under which the custom order function is registered
214 | */
215 | this.setSortFunction = function setSortFunction(sortFunctionName) {
216 | orderBy = $filter(sortFunctionName);
217 | };
218 |
219 | /**
220 | * Usually when the safe copy is updated the pipe function is called.
221 | * Calling this method will prevent it, which is something required when using a custom pipe function
222 | */
223 | this.preventPipeOnWatch = function preventPipe() {
224 | pipeAfterSafeCopy = false;
225 | };
226 | }
227 | ]).directive('stTable', function() {
228 | return {
229 | restrict: 'A',
230 | controller: 'stTableController',
231 | link: function(scope, element, attr, ctrl) {
232 | if (attr.stSetFilter) {
233 | ctrl.setFilterFunction(attr.stSetFilter);
234 | }
235 |
236 | if (attr.stSetSort) {
237 | ctrl.setSortFunction(attr.stSetSort);
238 | }
239 | }
240 | };
241 | });
242 |
--------------------------------------------------------------------------------
/test/spec/stTable.spec.js:
--------------------------------------------------------------------------------
1 | describe('st table Controller', function () {
2 |
3 | var dataSet;
4 | var scope;
5 | var ctrl;
6 | var childScope;
7 |
8 | beforeEach(module('smart-table'));
9 |
10 | describe('with a simple data-set', function () {
11 |
12 | beforeEach(inject(function ($rootScope, $controller, $filter, $parse) {
13 | dataSet = [
14 | {name: 'Renard', firstname: 'Laurent', age: 66},
15 | {name: 'Francoise', firstname: 'Frere', age: 99},
16 | {name: 'Renard', firstname: 'Olivier', age: 33},
17 | {name: 'Le Blond', firstname: 'Bob', age: 22},
18 | {name: 'Faivre', firstname: 'Blandine', age: 44}
19 | ];
20 | scope = $rootScope;
21 | childScope = scope.$new();
22 | scope.data = dataSet;
23 | ctrl = $controller('stTableController', {
24 | $scope: scope, $parse: $parse, $filter: $filter, $attrs: {
25 | stTable: 'data'
26 | }
27 | });
28 |
29 | }));
30 |
31 | describe('init', function(){
32 | it('should contain default tableState', function(){
33 | var defaultTableState = {
34 | sort: {},
35 | search: {},
36 | pagination: {
37 | start: 0,
38 | totalItemCount: 0
39 | }
40 | };
41 |
42 | var tableState = ctrl.tableState();
43 | expect(tableState).toEqual(defaultTableState);
44 | });
45 | });
46 |
47 | describe('sort', function () {
48 | it('should sort the data', function () {
49 | ctrl.sortBy('firstname');
50 | expect(scope.data).toEqual([
51 | {name: 'Faivre', firstname: 'Blandine', age: 44},
52 | {name: 'Le Blond', firstname: 'Bob', age: 22},
53 | {name: 'Francoise', firstname: 'Frere', age: 99},
54 | {name: 'Renard', firstname: 'Laurent', age: 66},
55 | {name: 'Renard', firstname: 'Olivier', age: 33}
56 | ]);
57 | });
58 |
59 | it('should reverse the order if the flag is passed', function () {
60 | ctrl.sortBy('firstname', true);
61 | expect(scope.data).toEqual([
62 | {name: 'Renard', firstname: 'Olivier', age: 33},
63 | {name: 'Renard', firstname: 'Laurent', age: 66},
64 | {name: 'Francoise', firstname: 'Frere', age: 99},
65 | {name: 'Le Blond', firstname: 'Bob', age: 22},
66 | {name: 'Faivre', firstname: 'Blandine', age: 44}
67 | ]);
68 | });
69 |
70 | it('should support getter function predicate', function () {
71 | ctrl.sortBy(function (row) {
72 | return row.firstname.length;
73 | });
74 | expect(scope.data).toEqual([
75 | {name: 'Le Blond', firstname: 'Bob', age: 22},
76 | {name: 'Francoise', firstname: 'Frere', age: 99},
77 | {name: 'Renard', firstname: 'Laurent', age: 66},
78 | {name: 'Renard', firstname: 'Olivier', age: 33},
79 | {name: 'Faivre', firstname: 'Blandine', age: 44}
80 | ]);
81 | });
82 |
83 | it('should hold the function name when using a function as predicate', function () {
84 | ctrl.sortBy(function firstNameLength(row) {
85 | return row.firstname.length;
86 | });
87 |
88 | expect(scope.data).toEqual([
89 | {name: 'Le Blond', firstname: 'Bob', age: 22},
90 | {name: 'Francoise', firstname: 'Frere', age: 99},
91 | {name: 'Renard', firstname: 'Laurent', age: 66},
92 | {name: 'Renard', firstname: 'Olivier', age: 33},
93 | {name: 'Faivre', firstname: 'Blandine', age: 44}
94 | ]);
95 |
96 | expect(ctrl.tableState().sort.functionName).toBe('firstNameLength');
97 |
98 | });
99 |
100 | it('should reset the function name when sorting with something than function', function () {
101 | ctrl.sortBy(function firstNameLength(row) {
102 | return row.firstname.length;
103 | });
104 | expect(ctrl.tableState().sort.functionName).toBe('firstNameLength');
105 | ctrl.sortBy('name');
106 | expect(ctrl.tableState().sort.functionName).toBe(undefined);
107 | expect(ctrl.tableState().sort.predicate).toBe('name');
108 |
109 | });
110 |
111 |
112 | });
113 |
114 | describe('search', function () {
115 | it('should search based on property name ', function () {
116 | ctrl.search('re', 'name');
117 | expect(scope.data).toEqual([
118 | {name: 'Renard', firstname: 'Laurent', age: 66},
119 | {name: 'Renard', firstname: 'Olivier', age: 33},
120 | {name: 'Faivre', firstname: 'Blandine', age: 44}
121 | ]);
122 | });
123 |
124 | it('should not filter out null value when input is empty string', inject(function ($controller, $parse, $filter) {
125 | scope.data = [
126 | {name: null, firstname: 'Laurent', age: 66},
127 | {name: 'Renard', firstname: 'Olivier', age: 33},
128 | {name: 'Faivre', firstname: 'Blandine', age: 44}
129 | ];
130 |
131 | //use another dataset for this particular spec
132 | ctrl = $controller('stTableController', {
133 | $scope: scope, $parse: $parse, $filter: $filter, $attrs: {
134 | stTable: 'data'
135 | }
136 | });
137 |
138 |
139 | ctrl.search('re', 'name');
140 | expect(scope.data).toEqual([
141 | {name: 'Renard', firstname: 'Olivier', age: 33},
142 | {name: 'Faivre', firstname: 'Blandine', age: 44}
143 | ]);
144 |
145 | ctrl.search('', 'name');
146 |
147 | expect(scope.data).toEqual([
148 | {name: null, firstname: 'Laurent', age: 66},
149 | {name: 'Renard', firstname: 'Olivier', age: 33},
150 | {name: 'Faivre', firstname: 'Blandine', age: 44}
151 | ]);
152 |
153 | }));
154 |
155 | it('should search globally', function () {
156 | ctrl.search('re');
157 | expect(scope.data).toEqual([
158 | {name: 'Renard', firstname: 'Laurent', age: 66},
159 | {name: 'Francoise', firstname: 'Frere', age: 99},
160 | {name: 'Renard', firstname: 'Olivier', age: 33},
161 | {name: 'Faivre', firstname: 'Blandine', age: 44}
162 | ])
163 | });
164 |
165 | it('should add different columns', function () {
166 | ctrl.search('re', 'name');
167 | expect(scope.data).toEqual([
168 | {name: 'Renard', firstname: 'Laurent', age: 66},
169 | {name: 'Renard', firstname: 'Olivier', age: 33},
170 | {name: 'Faivre', firstname: 'Blandine', age: 44}
171 | ]);
172 |
173 | ctrl.search('re', 'firstname');
174 |
175 | expect(scope.data).toEqual([
176 | {name: 'Renard', firstname: 'Laurent', age: 66}
177 | ]);
178 | });
179 |
180 | it('should search input string containing leading space', function () {
181 | ctrl.search(' blond', 'name', true);
182 | expect(scope.data).toEqual([
183 | {name: 'Le Blond', firstname: 'Bob', age: 22}
184 | ]);
185 | });
186 |
187 | it('should search input string containing trailing space', function () {
188 | ctrl.search('le ', 'name', true);
189 | expect(scope.data).toEqual([
190 | {name: 'Le Blond', firstname: 'Bob', age: 22}
191 | ]);
192 | });
193 |
194 | it('should search input string with no leading or trailing space', function () {
195 | ctrl.search('re', 'name', true);
196 | expect(scope.data).toEqual([
197 | {name: 'Renard', firstname: 'Laurent', age: 66},
198 | {name: 'Renard', firstname: 'Olivier', age: 33},
199 | {name: 'Faivre', firstname: 'Blandine', age: 44}
200 | ]);
201 | });
202 |
203 | it('should search when input is not a string', function () {
204 | ctrl.search(2, 'age');
205 | expect(scope.data).toEqual([
206 | {name: 'Le Blond', firstname: 'Bob', age: 22}
207 | ]);
208 | });
209 | });
210 |
211 | describe('slice', function () {
212 | it('should slice the collection', function () {
213 | ctrl.slice(1, 2);
214 | expect(scope.data.length).toBe(2);
215 | expect(scope.data).toEqual([
216 | {name: 'Francoise', firstname: 'Frere', age: 99},
217 | {name: 'Renard', firstname: 'Olivier', age: 33}
218 | ]);
219 | });
220 |
221 | it('limit to the last page if not enough data', function () {
222 | ctrl.slice(7, 2);
223 | expect(scope.data.length).toBe(1);
224 | expect(scope.data).toEqual([
225 | {name: 'Faivre', firstname: 'Blandine', age: 44}
226 | ]);
227 | });
228 | });
229 |
230 | describe('pipe', function () {
231 | it('should set totalItemCount on tableState pagination', function(){
232 | var expectedLength = 5;
233 | ctrl.pipe();
234 | expect(scope.data.length).toBe(expectedLength);
235 | expect(ctrl.tableState().pagination.totalItemCount).toBe(expectedLength);
236 | });
237 |
238 | it('should set totalItemCount as size of filtered array', function(){
239 | var expectedLength = 3;
240 | ctrl.search('re', 'name');
241 | expect(scope.data.length).toBe(expectedLength);
242 | expect(ctrl.tableState().pagination.totalItemCount).toBe(expectedLength);
243 | });
244 |
245 | it('should remembered the last slice length but start back to zero when sorting', function () {
246 | ctrl.slice(1, 2);
247 | expect(scope.data.length).toBe(2);
248 | expect(scope.data).toEqual([
249 | {name: 'Francoise', firstname: 'Frere', age: 99},
250 | {name: 'Renard', firstname: 'Olivier', age: 33}
251 | ]);
252 |
253 | ctrl.sortBy('firstname');
254 | expect(scope.data.length).toBe(2);
255 | expect(scope.data).toEqual([
256 | {name: 'Faivre', firstname: 'Blandine', age: 44},
257 | {name: 'Le Blond', firstname: 'Bob', age: 22}
258 | ]);
259 | });
260 |
261 | it('should remembered the last slice length but start back to zero when filtering', function () {
262 | ctrl.slice(1, 2);
263 | expect(scope.data.length).toBe(2);
264 | expect(scope.data).toEqual([
265 | {name: 'Francoise', firstname: 'Frere', age: 99},
266 | {name: 'Renard', firstname: 'Olivier', age: 33}
267 | ]);
268 |
269 | ctrl.search('re', 'name');
270 | expect(scope.data.length).toBe(2);
271 | expect(scope.data).toEqual([
272 | {name: 'Renard', firstname: 'Laurent', age: 66},
273 | {name: 'Renard', firstname: 'Olivier', age: 33}
274 | ]);
275 | });
276 |
277 | it('should remember sort state when filtering', function () {
278 | ctrl.sortBy('firstname');
279 | expect(scope.data).toEqual([
280 | {name: 'Faivre', firstname: 'Blandine', age: 44},
281 | {name: 'Le Blond', firstname: 'Bob', age: 22},
282 | {name: 'Francoise', firstname: 'Frere', age: 99},
283 | {name: 'Renard', firstname: 'Laurent', age: 66},
284 | {name: 'Renard', firstname: 'Olivier', age: 33}
285 | ]);
286 |
287 | ctrl.search('re', 'name');
288 | expect(scope.data).toEqual([
289 | {name: 'Faivre', firstname: 'Blandine', age: 44},
290 | {name: 'Renard', firstname: 'Laurent', age: 66},
291 | {name: 'Renard', firstname: 'Olivier', age: 33}
292 | ]);
293 |
294 | });
295 |
296 | it('should remember filtering when sorting', function () {
297 | ctrl.search('re', 'name');
298 | expect(scope.data).toEqual([
299 | {name: 'Renard', firstname: 'Laurent', age: 66},
300 | {name: 'Renard', firstname: 'Olivier', age: 33},
301 | {name: 'Faivre', firstname: 'Blandine', age: 44}
302 | ]);
303 | ctrl.sortBy('age');
304 | expect(scope.data).toEqual([
305 | {name: 'Renard', firstname: 'Olivier', age: 33},
306 | {name: 'Faivre', firstname: 'Blandine', age: 44},
307 | {name: 'Renard', firstname: 'Laurent', age: 66}
308 | ]);
309 | });
310 |
311 | it('should forward the search result collection', inject(function () {
312 | expect(ctrl.getFilteredCollection()).toEqual(dataSet)
313 | ctrl.search('re', 'name');
314 | expect(scope.data).toEqual([
315 | {name: 'Renard', firstname: 'Laurent', age: 66},
316 | {name: 'Renard', firstname: 'Olivier', age: 33},
317 | {name: 'Faivre', firstname: 'Blandine', age: 44}
318 | ]);
319 | expect(ctrl.getFilteredCollection()).toEqual([
320 | {name: 'Renard', firstname: 'Laurent', age: 66},
321 | {name: 'Renard', firstname: 'Olivier', age: 33},
322 | {name: 'Faivre', firstname: 'Blandine', age: 44}
323 | ])
324 | }));
325 | });
326 |
327 | describe('select', function () {
328 |
329 | function getSelected(array) {
330 | return array.filter(function (val) {
331 | return val.isSelected === true;
332 | });
333 | }
334 |
335 |
336 | it('should select only a single row at the time', function () {
337 | ctrl.select(scope.data[3], 'single');
338 | var selected = getSelected(scope.data);
339 | expect(selected.length).toBe(1);
340 | expect(selected[0]).toEqual(scope.data[3]);
341 |
342 | ctrl.select(scope.data[2], 'single');
343 |
344 | selected = getSelected(scope.data);
345 |
346 | expect(selected.length).toBe(1);
347 | expect(selected[0]).toEqual(scope.data[2]);
348 | });
349 |
350 | it('should select a row multiple times in single mode (#165)', function () {
351 | ctrl.select(scope.data[3], 'single');
352 | var selected = getSelected(scope.data);
353 | expect(selected.length).toBe(1);
354 | expect(selected[0]).toEqual(scope.data[3]);
355 |
356 | ctrl.select(scope.data[3], 'single');
357 | selected = getSelected(scope.data);
358 |
359 | expect(selected.length).toBe(0);
360 |
361 | ctrl.select(scope.data[3], 'single');
362 | selected = getSelected(scope.data);
363 |
364 | expect(selected.length).toBe(1);
365 | expect(selected[0]).toEqual(scope.data[3]);
366 | });
367 |
368 | it('should select multiple row', function () {
369 | ctrl.select(scope.data[3]);
370 | ctrl.select(scope.data[4]);
371 | var selected = getSelected(scope.data);
372 | expect(selected.length).toBe(2);
373 | expect(selected).toEqual([scope.data[3], scope.data[4]]);
374 | });
375 |
376 | it('should unselect an item on mode single', function () {
377 | ctrl.select(scope.data[3], 'single');
378 | var selected = getSelected(scope.data);
379 | expect(selected.length).toBe(1);
380 | expect(selected[0]).toEqual(scope.data[3]);
381 |
382 | ctrl.select(scope.data[3], 'single');
383 |
384 | selected = getSelected(scope.data);
385 |
386 | expect(selected.length).toBe(0);
387 | });
388 |
389 | it('should unselect an item on mode multiple', function () {
390 | ctrl.select(scope.data[3]);
391 | ctrl.select(scope.data[4]);
392 | var selected = getSelected(scope.data);
393 | expect(selected.length).toBe(2);
394 | expect(selected).toEqual([scope.data[3], scope.data[4]]);
395 |
396 | ctrl.select(scope.data[3]);
397 | selected = getSelected(scope.data);
398 | expect(selected.length).toBe(1);
399 | expect(selected).toEqual([scope.data[4]]);
400 | });
401 | });
402 | });
403 |
404 | describe('with safeSrc', function () {
405 | beforeEach(inject(function ($rootScope, $controller, $filter, $parse) {
406 | dataSet = [
407 | {name: 'Renard', firstname: 'Laurent', age: 66}
408 | ];
409 | scope = $rootScope;
410 | scope.data = dataSet;
411 | ctrl = $controller('stTableController', {
412 | $scope: scope, $parse: $parse, $filter: $filter, $attrs: {
413 | stTable: 'tableData',
414 | stSafeSrc: 'data'
415 | }
416 | });
417 | }));
418 |
419 | it('adds tableData to the scope', function () {
420 | scope.$digest();
421 | expect(scope.tableData).toBeDefined();
422 | expect(scope.tableData[0].name).toEqual('Renard');
423 | });
424 | });
425 |
426 | });
427 |
--------------------------------------------------------------------------------
/test/spec/stSearch.spec.js:
--------------------------------------------------------------------------------
1 | describe('stSearch Directive', function () {
2 |
3 | var rootScope;
4 | var scope;
5 | var element;
6 |
7 | function trToModel(trs) {
8 | return Array.prototype.map.call(trs, function (ele) {
9 | return {
10 | name: ele.cells[0].innerHTML,
11 | firstname: ele.cells[1].innerHTML,
12 | age: +(ele.cells[2].innerHTML)
13 | };
14 | });
15 | }
16 |
17 | beforeEach(module('smart-table'));
18 |
19 | var stConfig;
20 | beforeEach(inject(function (_stConfig_) {
21 | stConfig = _stConfig_;
22 | }));
23 |
24 | describe('string predicate', function () {
25 |
26 | beforeEach(inject(function ($compile, $rootScope) {
27 |
28 | rootScope = $rootScope;
29 | scope = $rootScope.$new();
30 | scope.rowCollection = [
31 | {name: 'Renard', firstname: 'Laurent', age: 66},
32 | {name: 'Francoise', firstname: 'Frere', age: 99},
33 | {name: 'Renard', firstname: 'Olivier', age: 33},
34 | {name: 'Le Blond', firstname: 'Bob', age: 22},
35 | {name: 'Faivre', firstname: 'Blandine', age: 44}
36 | ];
37 |
38 | var template = '';
54 |
55 | element = $compile(template)(scope);
56 | scope.$apply();
57 | }));
58 |
59 | it('should keep only items which matches', inject(function ($timeout) {
60 | var ths = element.find('th');
61 | var trs;
62 |
63 | var input = angular.element(ths[0].children[0]);
64 | input[0].value = 're';
65 | input.triggerHandler('input');
66 | trs = element.find('tr.test-filtered');
67 | expect(trs.length).toBe(5);
68 | $timeout.flush();
69 | trs = element.find('tr.test-filtered');
70 | expect(trs.length).toBe(3);
71 | expect(trToModel(trs)).toEqual([
72 | {name: 'Renard', firstname: 'Laurent', age: 66},
73 | {name: 'Renard', firstname: 'Olivier', age: 33},
74 | {name: 'Faivre', firstname: 'Blandine', age: 44}
75 | ]);
76 | }));
77 |
78 | it('should search globally', inject(function ($timeout) {
79 | var ths = element.find('th');
80 |
81 | var input = angular.element(ths[1].children[0]);
82 | input[0].value = 're';
83 | input.triggerHandler('input');
84 | $timeout.flush();
85 | trs = element.find('tr.test-filtered');
86 | expect(trs.length).toBe(4);
87 | expect(trToModel(trs)).toEqual([
88 | {name: 'Renard', firstname: 'Laurent', age: 66},
89 | {name: 'Francoise', firstname: 'Frere', age: 99},
90 | {name: 'Renard', firstname: 'Olivier', age: 33},
91 | {name: 'Faivre', firstname: 'Blandine', age: 44}
92 | ]);
93 | }));
94 |
95 | it('should keep leading space in search input by default', inject(function ($timeout) {
96 | var ths = element.find('th');
97 |
98 | var input = angular.element(ths[1].children[0]);
99 | input[0].value = ' bl';
100 | input.triggerHandler('input');
101 | $timeout.flush();
102 | trs = element.find('tr.test-filtered');
103 | expect(trs.length).toBe(1);
104 | expect(trToModel(trs)).toEqual([
105 | {name: 'Le Blond', firstname: 'Bob', age: 22}
106 | ]);
107 | }));
108 |
109 | it('should keep trailing space in search input by default', inject(function ($timeout) {
110 | var ths = element.find('th');
111 |
112 | var input = angular.element(ths[1].children[0]);
113 | input[0].value = 'le ';
114 | input.triggerHandler('input');
115 | $timeout.flush();
116 | trs = element.find('tr.test-filtered');
117 | expect(trs.length).toBe(1);
118 | expect(trToModel(trs)).toEqual([
119 | {name: 'Le Blond', firstname: 'Bob', age: 22}
120 | ]);
121 | }));
122 |
123 | it('should support trim leading and trailing space in search input', inject(function ($timeout, $rootScope, $compile) {
124 | var oldTrimSearch= stConfig.search.trimSearch;
125 | stConfig.search.trimSearch = true;
126 |
127 | // Since we must set the stCofig before compiling, we must recompile after configuring a delay
128 | scope = $rootScope.$new();
129 | scope.rowCollection = [
130 | {name: 'Renard', firstname: 'Laurent', age: 66},
131 | {name: 'Francoise', firstname: 'Frere', age: 99},
132 | {name: 'Renard', firstname: 'Olivier', age: 33},
133 | {name: 'Le Blond', firstname: 'Bob', age: 22},
134 | {name: 'Faivre', firstname: 'Blandine', age: 44}
135 | ];
136 |
137 | var template = '';
153 |
154 | element = $compile(template)(scope);
155 | scope.$apply();
156 | var ths = element.find('th');
157 |
158 | var input = angular.element(ths[1].children[0]);
159 | input[0].value = ' re ';
160 | input.triggerHandler('input');
161 |
162 | $timeout.flush();
163 | trs = element.find('tr.test-filtered');
164 | expect(trs.length).toBe(4);
165 | expect(trToModel(trs)).toEqual([
166 | {name: 'Renard', firstname: 'Laurent', age: 66},
167 | {name: 'Francoise', firstname: 'Frere', age: 99},
168 | {name: 'Renard', firstname: 'Olivier', age: 33},
169 | {name: 'Faivre', firstname: 'Blandine', age: 44}
170 | ]);
171 |
172 | stConfig.search.trimSearch = oldTrimSearch;
173 |
174 | }));
175 |
176 | it('should throttle searching to 400ms by default', inject(function ($timeout) {
177 | var ths = element.find('th');
178 |
179 | var input = angular.element(ths[1].children[0]);
180 | input[0].value = 're';
181 | input.triggerHandler('input');
182 |
183 | $timeout.flush(399);
184 | trs = element.find('tr.test-filtered');
185 | expect(trs.length).toBe(5);
186 |
187 | $timeout.flush(1);
188 | trs = element.find('tr.test-filtered');
189 | expect(trs.length).toBe(4);
190 | }));
191 |
192 | it('should throttle searching by stConfig.search.delay', inject(function ($timeout, $rootScope, $compile) {
193 | var oldDelay = stConfig.search.delay;
194 | stConfig.search.delay = 845;
195 |
196 | // Since we must set the stCofig before compiling, we must recompile after configuring a delay
197 | scope = $rootScope.$new();
198 | scope.rowCollection = [
199 | {name: 'Renard', firstname: 'Laurent', age: 66},
200 | {name: 'Francoise', firstname: 'Frere', age: 99},
201 | {name: 'Renard', firstname: 'Olivier', age: 33},
202 | {name: 'Le Blond', firstname: 'Bob', age: 22},
203 | {name: 'Faivre', firstname: 'Blandine', age: 44}
204 | ];
205 |
206 | var template = '';
222 |
223 | element = $compile(template)(scope);
224 | scope.$apply();
225 | var ths = element.find('th');
226 |
227 | var input = angular.element(ths[1].children[0]);
228 | input[0].value = 're';
229 | input.triggerHandler('input');
230 |
231 | $timeout.flush(844);
232 | trs = element.find('tr.test-filtered');
233 | expect(trs.length).toBe(5);
234 |
235 | $timeout.flush(1);
236 | trs = element.find('tr.test-filtered');
237 | expect(trs.length).toBe(4);
238 |
239 | stConfig.search.delay = oldDelay;
240 | }));
241 | });
242 |
243 | describe('binding predicate', function () {
244 |
245 | beforeEach(inject(function ($compile, $rootScope) {
246 |
247 | rootScope = $rootScope;
248 | scope = $rootScope.$new();
249 | scope.rowCollection = [
250 | {name: 'Renard', firstname: 'Laurent', age: 66},
251 | {name: 'Francoise', firstname: 'Frere', age: 99},
252 | {name: 'Renard', firstname: 'Olivier', age: 33},
253 | {name: 'Le Blond', firstname: 'Bob', age: 22},
254 | {name: 'Faivre', firstname: 'Blandine', age: 44}
255 | ];
256 |
257 | var template = '';
273 |
274 | element = $compile(template)(scope);
275 | scope.$apply();
276 | }));
277 |
278 | it('should support binding on search predicate', inject(function ($compile, $timeout) {
279 | scope.searchPredicate = 'name';
280 | var ths = element.find('th');
281 | var trs;
282 |
283 | var input = angular.element(ths[0].children[0]);
284 | input[0].value = 're';
285 | input.triggerHandler('input');
286 | trs = element.find('tr.test-filtered');
287 | expect(trs.length).toBe(5);
288 | $timeout.flush();
289 | trs = element.find('tr.test-filtered');
290 | expect(trs.length).toBe(3);
291 | expect(trToModel(trs)).toEqual([
292 | {name: 'Renard', firstname: 'Laurent', age: 66},
293 | {name: 'Renard', firstname: 'Olivier', age: 33},
294 | {name: 'Faivre', firstname: 'Blandine', age: 44}
295 | ]);
296 |
297 | scope.searchPredicate = 'firstname';
298 | scope.$apply();
299 | trs = element.find('tr.test-filtered');
300 | expect(trs.length).toBe(2);
301 | expect(trToModel(trs)).toEqual([
302 | {name: 'Renard', firstname: 'Laurent', age: 66},
303 | {name: 'Francoise', firstname: 'Frere', age: 99}
304 | ]);
305 |
306 | }));
307 | });
308 |
309 | describe('deep object predicate', function () {
310 |
311 | beforeEach(inject(function ($compile, $rootScope) {
312 |
313 | rootScope = $rootScope;
314 | scope = $rootScope.$new();
315 | scope.rowCollection = [
316 | {name: {lastname: 'Renard', firstname: 'Laurent'}, age: 66, description:'really silly description'},
317 | {name: {lastname: 'Francoise', firstname: 'Frere'}, age: 99, description:'really silly description'},
318 | {name: {lastname: 'Renard', firstname: 'Olivier'}, age: 33, description:'really silly description'},
319 | {name: {lastname: 'Le Blond', firstname: 'Bob'}, age: 22, description:'really silly description'},
320 | {name: {lastname: 'Faivre', firstname: 'Blandine'}, age: 44, description:'really silly description'},
321 | {name: null, age: 33, description:'really silly description'}
322 | ];
323 |
324 | var template = '';
340 |
341 | element = $compile(template)(scope);
342 | scope.$apply();
343 | }));
344 |
345 | it('should keep only items which matches', inject(function ($timeout) {
346 | var ths = element.find('th');
347 | var trs;
348 |
349 | var input = angular.element(ths[0].children[0]);
350 | input[0].value = 're';
351 | input.triggerHandler('input');
352 | trs = element.find('tr.test-filtered');
353 | expect(trs.length).toBe(6);
354 | $timeout.flush();
355 | trs = element.find('tr.test-filtered');
356 | expect(trs.length).toBe(3);
357 | expect(trToModel(trs)).toEqual([
358 | {name: 'Renard', firstname: 'Laurent', age: 66},
359 | {name: 'Renard', firstname: 'Olivier', age: 33},
360 | {name: 'Faivre', firstname: 'Blandine', age: 44}
361 | ]);
362 | }));
363 |
364 | it('should search globally within the name object', inject(function ($timeout) {
365 | var ths = element.find('th');
366 |
367 | var input = angular.element(ths[1].children[0]);
368 | input[0].value = 're';
369 | input.triggerHandler('input');
370 | $timeout.flush();
371 | var trs = element.find('tr.test-filtered');
372 | expect(trs.length).toBe(4);
373 | expect(trToModel(trs)).toEqual([
374 | {name: 'Renard', firstname: 'Laurent', age: 66},
375 | {name: 'Francoise', firstname: 'Frere', age: 99},
376 | {name: 'Renard', firstname: 'Olivier', age: 33},
377 | {name: 'Faivre', firstname: 'Blandine', age: 44}
378 | ]);
379 | }));
380 |
381 | it('should be able to reset deep paths', inject(function ($timeout) {
382 | var ths = element.find('th');
383 |
384 | var input = angular.element(ths[0].children[0]);
385 | input[0].value = 're';
386 | input.triggerHandler('input');
387 | $timeout.flush();
388 | input[0].value = '';
389 | input.triggerHandler('input');
390 | $timeout.flush();
391 | trs = element.find('tr.test-filtered');
392 | expect(trs.length).toBe(6);
393 | }));
394 | });
395 | });
396 |
--------------------------------------------------------------------------------
/test/spec/stPagination.spec.js:
--------------------------------------------------------------------------------
1 | describe('stPagination directive', function () {
2 |
3 | var controllerMock = {
4 | tableState: function () {
5 | return tableState
6 | },
7 |
8 | slice: function (start, number) {
9 | tableState.pagination.start = start;
10 | tableState.pagination.number = number;
11 | }
12 | };
13 |
14 | function ControllerMock() {
15 | this.tableState = controllerMock.tableState;
16 | this.slice = controllerMock.slice;
17 | }
18 |
19 | var tableState = {
20 | sort: {},
21 | search: {},
22 | pagination: {start: 0, totalItemCount: 0}
23 | };
24 | var compile;
25 |
26 | function getPages() {
27 | return Array.prototype.map.call(element.find('LI'), function (val) {
28 | return angular.element(val);
29 | });
30 | }
31 |
32 |
33 | function hasClass(element, classname) {
34 | return Array.prototype.indexOf.call(element.classList, classname) !== -1
35 | }
36 |
37 | var rootScope;
38 | var element;
39 | var stConfig;
40 |
41 | beforeEach(module('smart-table', function ($controllerProvider) {
42 | $controllerProvider.register('stTableController', ControllerMock);
43 | }));
44 |
45 | beforeEach(inject(function ($compile, $rootScope, _stConfig_) {
46 | compile = $compile;
47 | rootScope = $rootScope;
48 | stConfig = _stConfig_;
49 |
50 | rootScope.rowCollection = [
51 | {name: 'Renard', firstname: 'Laurent', age: 66},
52 | {name: 'Francoise', firstname: 'Frere', age: 99},
53 | {name: 'Renard', firstname: 'Olivier', age: 33},
54 | {name: 'Leponge', firstname: 'Bob', age: 22},
55 | {name: 'Faivre', firstname: 'Blandine', age: 44}
56 | ];
57 | }));
58 |
59 | describe('init', function () {
60 | it('should call for the first page on init', function () {
61 | spyOn(controllerMock, 'slice');
62 | var template = '';
63 | compile(template)(rootScope);
64 | rootScope.$apply();
65 | element = angular.element(document.getElementById('pagination'));
66 | expect(controllerMock.slice).toHaveBeenCalledWith(0, 5);
67 | });
68 |
69 | it('should call for the first page with 10 as default value for item by page', function () {
70 | spyOn(controllerMock, 'slice');
71 | var template = '';
72 | compile(template)(rootScope);
73 | rootScope.$apply();
74 | element = angular.element(document.getElementById('pagination'));
75 | expect(controllerMock.slice).toHaveBeenCalledWith(0, 10);
76 | });
77 |
78 | it('should call for the first page with `stConfig.pagination.itemsByPage = 21` as value for item by page', function () {
79 | var oldItemsByPage = stConfig.pagination.itemsByPage;
80 | stConfig.pagination.itemsByPage = 21;
81 |
82 | spyOn(controllerMock, 'slice');
83 | var template = '';
84 | compile(template)(rootScope);
85 | rootScope.$apply();
86 | element = angular.element(document.getElementById('pagination'));
87 | expect(controllerMock.slice).toHaveBeenCalledWith(0, 21);
88 |
89 | stConfig.pagination.itemsByPage = oldItemsByPage;
90 | });
91 | });
92 |
93 | describe('template', function () {
94 | var templateCache;
95 | beforeEach(inject(function ($templateCache) {
96 | templateCache = $templateCache;
97 | }));
98 |
99 | it('should load custom template from stConfig\'s "st-template"', function () {
100 | var defaultTemplate = stConfig.pagination.template;
101 | stConfig.pagination.template = 'custom_stConfig_template.html';
102 |
103 | templateCache.put('custom_stConfig_template.html', '
');
104 |
105 | var template = '';
106 | element = compile(template)(rootScope);
107 | rootScope.$apply();
108 |
109 | expect(angular.element(element.find('div#custom_stConfig_id')).length).toBe(1);
110 |
111 | stConfig.pagination.template = defaultTemplate;
112 | });
113 |
114 | it('should load custom template from attribute "st-template"', function () {
115 | templateCache.put('custom_template.html', '
');
116 |
117 | var template = '';
118 | element = compile(template)(rootScope);
119 | rootScope.$apply();
120 |
121 | expect(angular.element(element.find('div#custom_id')).length).toBe(1);
122 | });
123 | });
124 |
125 | describe('draw pages', function () {
126 |
127 | it('it should draw the pages based on table state using 5 as default value for displayed pages number and center it on the active page', function () {
128 | var template = '';
129 | element = compile(template)(rootScope);
130 | rootScope.$apply();
131 |
132 | tableState.pagination = {
133 | start: 35,
134 | numberOfPages: 12,
135 | number: 10
136 | };
137 |
138 | rootScope.$apply();
139 |
140 | var pages = getPages();
141 |
142 | expect(pages.length).toBe(5);
143 |
144 | expect(pages[0].text()).toEqual('2');
145 | expect(pages[1].text()).toEqual('3');
146 | expect(pages[2].text()).toEqual('4');
147 | expect(pages[3].text()).toEqual('5');
148 | expect(pages[4].text()).toEqual('6');
149 |
150 | var active = pages.filter(function (value) {
151 | return hasClass(value[0], 'active');
152 | });
153 |
154 | expect(active.length).toBe(1);
155 | expect(active[0].text()).toEqual('4');
156 |
157 | });
158 |
159 | it('it should draw the pages based on table state using provided value for displayed pages', function () {
160 | var template = '';
161 | element = compile(template)(rootScope);
162 |
163 | rootScope.$apply();
164 |
165 | tableState.pagination = {
166 | start: 35,
167 | numberOfPages: 12,
168 | number: 10
169 | };
170 |
171 | rootScope.$apply();
172 |
173 | var pages = getPages();
174 |
175 | expect(pages.length).toBe(7);
176 |
177 | expect(pages[0].text()).toEqual('1');
178 | expect(pages[1].text()).toEqual('2');
179 | expect(pages[2].text()).toEqual('3');
180 | expect(pages[3].text()).toEqual('4');
181 | expect(pages[4].text()).toEqual('5');
182 | expect(pages[5].text()).toEqual('6');
183 | expect(pages[6].text()).toEqual('7');
184 |
185 | var active = pages.filter(function (value) {
186 | return hasClass(value[0], 'active');
187 | });
188 |
189 | expect(active.length).toBe(1);
190 | expect(active[0].text()).toEqual('4');
191 | });
192 |
193 | it('it should draw the pages based on stConfig default for displayed pages', function () {
194 | var defaultDisplayedPages = stConfig.pagination.displayedPages;
195 | stConfig.pagination.displayedPages = 8;
196 |
197 | var template = '';
198 | element = compile(template)(rootScope);
199 |
200 | rootScope.$apply();
201 |
202 | tableState.pagination = {
203 | start: 35,
204 | numberOfPages: 12,
205 | number: 10
206 | };
207 |
208 | rootScope.$apply();
209 |
210 | var pages = getPages();
211 |
212 | expect(pages.length).toBe(8);
213 |
214 | expect(pages[0].text()).toEqual('1');
215 | expect(pages[1].text()).toEqual('2');
216 | expect(pages[2].text()).toEqual('3');
217 | expect(pages[3].text()).toEqual('4');
218 | expect(pages[4].text()).toEqual('5');
219 | expect(pages[5].text()).toEqual('6');
220 | expect(pages[6].text()).toEqual('7');
221 | expect(pages[7].text()).toEqual('8');
222 |
223 | var active = pages.filter(function (value) {
224 | return hasClass(value[0], 'active');
225 | });
226 |
227 | expect(active.length).toBe(1);
228 | expect(active[0].text()).toEqual('4');
229 |
230 | stConfig.pagination.displayedPages = defaultDisplayedPages;
231 | });
232 |
233 | it('should support string for number and displayed page as this var can be bound', function () {
234 | rootScope.itemsByPage = "12";
235 | rootScope.displayedPages = "6";
236 | var template = '';
237 | element = compile(template)(rootScope);
238 |
239 | rootScope.$apply();
240 |
241 | tableState.pagination.start = 12;
242 | tableState.pagination.numberOfPages = 12;
243 |
244 | rootScope.$apply();
245 |
246 | var pages = getPages();
247 |
248 | expect(pages.length).toBe(6);
249 |
250 | expect(pages[0].text()).toEqual('1');
251 | expect(pages[1].text()).toEqual('2');
252 | expect(pages[2].text()).toEqual('3');
253 | expect(pages[3].text()).toEqual('4');
254 | expect(pages[4].text()).toEqual('5');
255 | expect(pages[5].text()).toEqual('6');
256 |
257 | var active = pages.filter(function (value) {
258 | return hasClass(value[0], 'active');
259 | });
260 |
261 | expect(active.length).toBe(1);
262 | expect(active[0].text()).toEqual('2');
263 | });
264 |
265 | it('it should not center when reaching higher edge', function () {
266 | var template = '';
267 | element = compile(template)(rootScope);
268 |
269 | rootScope.$apply();
270 |
271 | tableState.pagination = {
272 | start: 105,
273 | numberOfPages: 12,
274 | number: 10
275 | };
276 |
277 | rootScope.$apply();
278 |
279 |
280 | var pages = getPages();
281 |
282 | expect(pages.length).toBe(5);
283 |
284 | expect(pages[0].text()).toEqual('8');
285 | expect(pages[1].text()).toEqual('9');
286 | expect(pages[2].text()).toEqual('10');
287 | expect(pages[3].text()).toEqual('11');
288 | expect(pages[4].text()).toEqual('12');
289 |
290 | var active = pages.filter(function (value) {
291 | return hasClass(value[0], 'active');
292 | });
293 |
294 | expect(active.length).toBe(1);
295 | expect(active[0].text()).toEqual('11');
296 |
297 | });
298 |
299 | it('it should not center when reaching lower edge', function () {
300 | var template = '';
301 | element = compile(template)(rootScope);
302 |
303 | rootScope.$apply();
304 |
305 | tableState.pagination = {
306 | start: 5,
307 | numberOfPages: 12,
308 | number: 10
309 | };
310 |
311 | rootScope.$apply();
312 |
313 |
314 | var pages = getPages();
315 |
316 | expect(pages.length).toBe(5);
317 |
318 | expect(pages[0].text()).toEqual('1');
319 | expect(pages[1].text()).toEqual('2');
320 | expect(pages[2].text()).toEqual('3');
321 | expect(pages[3].text()).toEqual('4');
322 | expect(pages[4].text()).toEqual('5');
323 |
324 | var active = pages.filter(function (value) {
325 | return hasClass(value[0], 'active');
326 | });
327 |
328 | expect(active.length).toBe(1);
329 | expect(active[0].text()).toEqual('1');
330 |
331 | });
332 |
333 | it('it should limit the number of page to available pages', function () {
334 | var template = '';
335 | element = compile(template)(rootScope);
336 |
337 | rootScope.$apply();
338 |
339 | tableState.pagination = {
340 | start: 12,
341 | numberOfPages: 3,
342 | number: 10
343 | };
344 |
345 | rootScope.$apply();
346 |
347 |
348 | var pages = getPages();
349 |
350 | expect(pages.length).toBe(3);
351 |
352 | expect(pages[0].text()).toEqual('1');
353 | expect(pages[1].text()).toEqual('2');
354 | expect(pages[2].text()).toEqual('3');
355 |
356 | var active = pages.filter(function (value) {
357 | return hasClass(value[0], 'active');
358 | });
359 |
360 | expect(active.length).toBe(1);
361 | expect(active[0].text()).toEqual('2');
362 | });
363 |
364 | it('it should remove pagination when there is less than two pages', function () {
365 | var template = '';
366 | element = compile(template)(rootScope);
367 |
368 | rootScope.$apply();
369 |
370 | tableState.pagination = {
371 | start: 5,
372 | numberOfPages: 1,
373 | number: 10
374 | };
375 |
376 | rootScope.$apply();
377 |
378 | var pages = getPages();
379 |
380 | expect(pages.length).toBe(0);
381 | });
382 |
383 | describe('with extended template', function () {
384 | var templateCache;
385 | beforeEach(inject(function ($templateCache) {
386 | templateCache = $templateCache;
387 | }));
388 |
389 | it('should save totalItemCount from paginationState on scope', function () {
390 | templateCache.put('custom_template.html', '' +
393 | 'Showing {{(currentPage-1)*stItemsByPage+1}}-{{(currentPage)*stItemsByPage > totalItemCount ? totalItemCount : (currentPage)*stItemsByPage}} of {{totalItemCount}} ' +
394 | ' ');
395 |
396 | var template = '';
397 | element = compile(template)(rootScope);
398 |
399 | rootScope.$apply();
400 |
401 | tableState.pagination = {
402 | start: 3,
403 | numberOfPages: 3,
404 | number: 2,
405 | totalItemCount: 5
406 | };
407 |
408 | rootScope.$apply();
409 |
410 | expect(element.find('SPAN')[0].innerHTML).toBe('Showing 3-4 of 5');
411 | });
412 | });
413 | });
414 |
415 | describe('select page', function () {
416 |
417 | it('should select a page', function () {
418 |
419 | spyOn(controllerMock, 'slice').and.callThrough();
420 |
421 | var template = '';
422 | element = compile(template)(rootScope);
423 |
424 | rootScope.$apply();
425 |
426 | tableState.pagination = {
427 | start: 35,
428 | numberOfPages: 12,
429 | number: 10
430 | };
431 |
432 | rootScope.$apply();
433 |
434 | var pages = getPages();
435 |
436 | expect(pages.length).toBe(5);
437 |
438 | expect(pages[0].text()).toEqual('2');
439 | expect(pages[1].text()).toEqual('3');
440 | expect(pages[2].text()).toEqual('4');
441 | expect(pages[3].text()).toEqual('5');
442 | expect(pages[4].text()).toEqual('6');
443 |
444 | var active = pages.filter(function (value) {
445 | return hasClass(value[0], 'active');
446 | });
447 |
448 | expect(active.length).toBe(1);
449 | expect(active[0].text()).toEqual('4');
450 |
451 | angular.element(pages[4].children()[0]).triggerHandler('click');
452 |
453 | rootScope.$apply();
454 |
455 | expect(controllerMock.slice).toHaveBeenCalledWith(50, 10);
456 |
457 | });
458 |
459 | });
460 |
461 | describe('call onPageChange callback', function () {
462 |
463 | it('should call onPageChange method', function () {
464 | rootScope.onPageChange = jasmine.createSpy('onPageChange');
465 | spyOn(controllerMock, 'slice').and.callThrough();
466 |
467 | tableState.pagination = {
468 | start: 1,
469 | numberOfPages: 4,
470 | number: 10
471 | };
472 |
473 | var template = '';
474 | element = compile(template)(rootScope);
475 |
476 | rootScope.$apply();
477 |
478 | var pages = getPages();
479 |
480 | expect(pages.length).toBe(4);
481 | angular.element(pages[2].children()[0]).triggerHandler('click');
482 |
483 | rootScope.$apply();
484 |
485 | expect(controllerMock.slice).toHaveBeenCalledWith(20, 10);
486 | expect(rootScope.onPageChange).toHaveBeenCalledWith(3);
487 | expect(rootScope.onPageChange.calls.count()).toBe(1);
488 |
489 | });
490 |
491 | it('should should not call when current page is not changed', function () {
492 |
493 | rootScope.onPageChange = jasmine.createSpy('onPageChange');
494 | spyOn(controllerMock, 'slice').and.callThrough();
495 |
496 | tableState.pagination = {
497 | start: 1,
498 | numberOfPages: 4,
499 | number: 10
500 | };
501 |
502 | var template = '';
503 | element = compile(template)(rootScope);
504 |
505 | rootScope.$apply();
506 |
507 | var pages = getPages();
508 |
509 | expect(pages.length).toBe(4);
510 | angular.element(pages[2].children()[0]).triggerHandler('click');
511 |
512 | rootScope.$apply();
513 |
514 | expect(controllerMock.slice).toHaveBeenCalledWith(20, 10);
515 | expect(rootScope.onPageChange).toHaveBeenCalledWith(3);
516 | expect(rootScope.onPageChange.calls.count()).toBe(1);
517 |
518 | tableState.pagination.numberOfPages = 5;
519 |
520 | rootScope.$apply();
521 | pages = getPages();
522 |
523 | expect(pages.length).toBe(5);
524 | expect(rootScope.onPageChange.calls.count()).toBe(1);
525 | });
526 |
527 | });
528 |
529 | });
530 |
--------------------------------------------------------------------------------
/dist/smart-table.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @version 2.1.11
3 | * @license MIT
4 | */
5 | (function (ng, undefined){
6 | 'use strict';
7 |
8 | ng.module('smart-table', []).run(['$templateCache', function ($templateCache) {
9 | $templateCache.put('template/smart-table/pagination.html',
10 | ' ');
13 | }]);
14 |
15 |
16 | ng.module('smart-table')
17 | .constant('stConfig', {
18 | pagination: {
19 | template: 'template/smart-table/pagination.html',
20 | itemsByPage: 10,
21 | displayedPages: 5
22 | },
23 | search: {
24 | delay: 400, // ms
25 | inputEvent: 'input',
26 | trimSearch: false
27 | },
28 | select: {
29 | mode: 'single',
30 | selectedClass: 'st-selected'
31 | },
32 | sort: {
33 | ascentClass: 'st-sort-ascent',
34 | descentClass: 'st-sort-descent',
35 | descendingFirst: false,
36 | skipNatural: false,
37 | delay:300
38 | },
39 | pipe: {
40 | delay: 100 //ms
41 | }
42 | });
43 | ng.module('smart-table').controller('stTableController', [
44 | '$scope',
45 | '$parse',
46 | '$filter',
47 | '$attrs',
48 | function StTableController($scope, $parse, $filter, $attrs) {
49 | var propertyName = $attrs.stTable;
50 | var displayGetter = $parse(propertyName);
51 | var displaySetter = displayGetter.assign;
52 | var safeGetter;
53 | var orderBy = $filter('orderBy');
54 | var filter = $filter('filter');
55 | var safeCopy = copyRefs(displayGetter($scope));
56 | var tableState = {
57 | sort: {},
58 | search: {},
59 | pagination: { start: 0, totalItemCount: 0 }
60 | };
61 | var filtered;
62 | var pipeAfterSafeCopy = true;
63 | var ctrl = this;
64 | var lastSelected;
65 |
66 | function copyRefs(src) {
67 | return src ? [].concat(src) : [];
68 | }
69 |
70 | function updateSafeCopy() {
71 | safeCopy = copyRefs(safeGetter($scope));
72 | if (pipeAfterSafeCopy === true) {
73 | ctrl.pipe();
74 | }
75 | }
76 |
77 | function deepDelete(object, path) {
78 | if (path.indexOf('.') != -1) {
79 | var partials = path.split('.');
80 | var key = partials.pop();
81 | var parentPath = partials.join('.');
82 | var parentObject = $parse(parentPath)(object);
83 | delete parentObject[key];
84 | if (Object.keys(parentObject).length == 0) {
85 | deepDelete(object, parentPath);
86 | }
87 | } else {
88 | delete object[path];
89 | }
90 | }
91 |
92 | if ($attrs.stSafeSrc) {
93 | safeGetter = $parse($attrs.stSafeSrc);
94 | $scope.$watch(
95 | function() {
96 | var safeSrc = safeGetter($scope);
97 | return safeSrc && safeSrc.length ? safeSrc[0] : undefined;
98 | },
99 | function(newValue, oldValue) {
100 | if (newValue !== oldValue) {
101 | updateSafeCopy();
102 | }
103 | }
104 | );
105 | $scope.$watch(
106 | function() {
107 | var safeSrc = safeGetter($scope);
108 | return safeSrc ? safeSrc.length : 0;
109 | },
110 | function(newValue, oldValue) {
111 | if (newValue !== safeCopy.length) {
112 | updateSafeCopy();
113 | }
114 | }
115 | );
116 | $scope.$watch(
117 | function() {
118 | return safeGetter($scope);
119 | },
120 | function(newValue, oldValue) {
121 | if (newValue !== oldValue) {
122 | tableState.pagination.start = 0;
123 | updateSafeCopy();
124 | }
125 | }
126 | );
127 | }
128 |
129 | /**
130 | * sort the rows
131 | * @param {Function | String} predicate - function or string which will be used as predicate for the sorting
132 | * @param [reverse] - if you want to reverse the order
133 | */
134 | this.sortBy = function sortBy(predicate, reverse) {
135 | tableState.sort.predicate = predicate;
136 | tableState.sort.reverse = reverse === true;
137 |
138 | if (ng.isFunction(predicate)) {
139 | tableState.sort.functionName = predicate.name;
140 | } else {
141 | delete tableState.sort.functionName;
142 | }
143 |
144 | tableState.pagination.start = 0;
145 | return this.pipe();
146 | };
147 |
148 | /**
149 | * search matching rows
150 | * @param {String} input - the input string
151 | * @param {String} [predicate] - the property name against you want to check the match, otherwise it will search on all properties
152 | * @param {String | Function } [comparator] - a comparator to pass to the filter for the (pass true for stric mode)
153 | */
154 | this.search = function search(input, predicate, comparator) {
155 | var predicateObject = tableState.search.predicateObject || {};
156 | var prop = predicate ? predicate : '$';
157 |
158 | $parse(prop).assign(predicateObject, input);
159 | // to avoid to filter out null value
160 | if (!input) {
161 | deepDelete(predicateObject, prop);
162 | }
163 | tableState.search.predicateObject = predicateObject;
164 | tableState.pagination.start = 0;
165 | return this.pipe();
166 | };
167 |
168 | /**
169 | * this will chain the operations of sorting and filtering based on the current table state (sort options, filtering, ect)
170 | */
171 | this.pipe = function pipe() {
172 | var pagination = tableState.pagination;
173 | var output;
174 | filtered = tableState.search.predicateObject
175 | ? filter(safeCopy, tableState.search.predicateObject)
176 | : safeCopy;
177 | if (tableState.sort.predicate) {
178 | filtered = orderBy(
179 | filtered,
180 | tableState.sort.predicate,
181 | tableState.sort.reverse
182 | );
183 | }
184 | pagination.totalItemCount = filtered.length;
185 | if (pagination.number !== undefined) {
186 | pagination.numberOfPages = filtered.length > 0
187 | ? Math.ceil(filtered.length / pagination.number)
188 | : 1;
189 | pagination.start = pagination.start >= filtered.length
190 | ? (pagination.numberOfPages - 1) * pagination.number
191 | : pagination.start;
192 | output = filtered.slice(
193 | pagination.start,
194 | pagination.start + parseInt(pagination.number)
195 | );
196 | }
197 | displaySetter($scope, output || filtered);
198 | };
199 |
200 | /**
201 | * select a dataRow (it will add the attribute isSelected to the row object)
202 | * @param {Object} row - the row to select
203 | * @param {String} [mode] - "single" or "multiple" (multiple by default)
204 | */
205 | this.select = function select(row, mode) {
206 | var rows = copyRefs(displayGetter($scope));
207 | var index = rows.indexOf(row);
208 | if (index !== -1) {
209 | if (mode === 'single') {
210 | row.isSelected = row.isSelected !== true;
211 | if (lastSelected) {
212 | lastSelected.isSelected = false;
213 | }
214 | lastSelected = row.isSelected === true ? row : undefined;
215 | } else {
216 | rows[index].isSelected = !rows[index].isSelected;
217 | }
218 | }
219 | };
220 |
221 | /**
222 | * take a slice of the current sorted/filtered collection (pagination)
223 | *
224 | * @param {Number} start - start index of the slice
225 | * @param {Number} number - the number of item in the slice
226 | */
227 | this.slice = function splice(start, number) {
228 | tableState.pagination.start = start;
229 | tableState.pagination.number = number;
230 | return this.pipe();
231 | };
232 |
233 | /**
234 | * return the current state of the table
235 | * @returns {{sort: {}, search: {}, pagination: {start: number}}}
236 | */
237 | this.tableState = function getTableState() {
238 | return tableState;
239 | };
240 |
241 | this.getFilteredCollection = function getFilteredCollection() {
242 | return filtered || safeCopy;
243 | };
244 |
245 | /**
246 | * Use a different filter function than the angular FilterFilter
247 | * @param filterName the name under which the custom filter is registered
248 | */
249 | this.setFilterFunction = function setFilterFunction(filterName) {
250 | filter = $filter(filterName);
251 | };
252 |
253 | /**
254 | * Use a different function than the angular orderBy
255 | * @param sortFunctionName the name under which the custom order function is registered
256 | */
257 | this.setSortFunction = function setSortFunction(sortFunctionName) {
258 | orderBy = $filter(sortFunctionName);
259 | };
260 |
261 | /**
262 | * Usually when the safe copy is updated the pipe function is called.
263 | * Calling this method will prevent it, which is something required when using a custom pipe function
264 | */
265 | this.preventPipeOnWatch = function preventPipe() {
266 | pipeAfterSafeCopy = false;
267 | };
268 | }
269 | ]).directive('stTable', function() {
270 | return {
271 | restrict: 'A',
272 | controller: 'stTableController',
273 | link: function(scope, element, attr, ctrl) {
274 | if (attr.stSetFilter) {
275 | ctrl.setFilterFunction(attr.stSetFilter);
276 | }
277 |
278 | if (attr.stSetSort) {
279 | ctrl.setSortFunction(attr.stSetSort);
280 | }
281 | }
282 | };
283 | });
284 |
285 | ng.module('smart-table')
286 | .directive('stSearch', ['stConfig', '$timeout','$parse', function (stConfig, $timeout, $parse) {
287 | return {
288 | require: '^stTable',
289 | link: function (scope, element, attr, ctrl) {
290 | var tableCtrl = ctrl;
291 | var promise = null;
292 | var throttle = attr.stDelay || stConfig.search.delay;
293 | var event = attr.stInputEvent || stConfig.search.inputEvent;
294 | var trimSearch = attr.trimSearch || stConfig.search.trimSearch;
295 |
296 | attr.$observe('stSearch', function (newValue, oldValue) {
297 | var input = element[0].value;
298 | if (newValue !== oldValue && input) {
299 | ctrl.tableState().search = {};
300 | input = ng.isString(input) && trimSearch ? input.trim() : input;
301 | tableCtrl.search(input, newValue);
302 | }
303 | });
304 |
305 | //table state -> view
306 | scope.$watch(function () {
307 | return ctrl.tableState().search;
308 | }, function (newValue, oldValue) {
309 | var predicateExpression = attr.stSearch || '$';
310 | if (newValue.predicateObject && $parse(predicateExpression)(newValue.predicateObject) !== element[0].value) {
311 | element[0].value = $parse(predicateExpression)(newValue.predicateObject) || '';
312 | }
313 | }, true);
314 |
315 | // view -> table state
316 | element.bind(event, function (evt) {
317 | evt = evt.originalEvent || evt;
318 | if (promise !== null) {
319 | $timeout.cancel(promise);
320 | }
321 |
322 | promise = $timeout(function () {
323 | var input = evt.target.value;
324 | input = ng.isString(input) && trimSearch ? input.trim() : input;
325 | tableCtrl.search(input, attr.stSearch || '');
326 | promise = null;
327 | }, throttle);
328 | });
329 | }
330 | };
331 | }]);
332 |
333 | ng.module('smart-table')
334 | .directive('stSelectRow', ['stConfig', function (stConfig) {
335 | return {
336 | restrict: 'A',
337 | require: '^stTable',
338 | scope: {
339 | row: '=stSelectRow'
340 | },
341 | link: function (scope, element, attr, ctrl) {
342 | var mode = attr.stSelectMode || stConfig.select.mode;
343 | element.bind('click', function () {
344 | scope.$apply(function () {
345 | ctrl.select(scope.row, mode);
346 | });
347 | });
348 |
349 | scope.$watch('row.isSelected', function (newValue) {
350 | if (newValue === true) {
351 | element.addClass(stConfig.select.selectedClass);
352 | } else {
353 | element.removeClass(stConfig.select.selectedClass);
354 | }
355 | });
356 | }
357 | };
358 | }]);
359 |
360 | ng.module('smart-table')
361 | .directive('stSort', ['stConfig', '$parse', '$timeout', function (stConfig, $parse, $timeout) {
362 | return {
363 | restrict: 'A',
364 | require: '^stTable',
365 | link: function (scope, element, attr, ctrl) {
366 |
367 | var predicate = attr.stSort;
368 | var getter = $parse(predicate);
369 | var index = 0;
370 | var classAscent = attr.stClassAscent || stConfig.sort.ascentClass;
371 | var classDescent = attr.stClassDescent || stConfig.sort.descentClass;
372 | var stateClasses = [classAscent, classDescent];
373 | var sortDefault;
374 | var skipNatural = attr.stSkipNatural !== undefined ? attr.stSkipNatural : stConfig.sort.skipNatural;
375 | var descendingFirst = attr.stDescendingFirst !== undefined ? attr.stDescendingFirst : stConfig.sort.descendingFirst;
376 | var promise = null;
377 | var throttle = attr.stDelay || stConfig.sort.delay;
378 |
379 | // set aria attributes
380 | var ariaSort = 'aria-sort';
381 | var ariaSortNone = 'none';
382 | var ariaSortAscending = 'ascending';
383 | var ariaSortDescending = 'descending';
384 | element
385 | .attr('role', 'columnheader')
386 | .attr(ariaSort, ariaSortNone);
387 |
388 | if (attr.stSortDefault) {
389 | sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;
390 | }
391 |
392 | //view --> table state
393 | function sort () {
394 | if (descendingFirst) {
395 | index = index === 0 ? 2 : index - 1;
396 | } else {
397 | index++;
398 | }
399 |
400 | var func;
401 | predicate = ng.isFunction(getter(scope)) || ng.isArray(getter(scope)) ? getter(scope) : attr.stSort;
402 | if (index % 3 === 0 && !!skipNatural !== true) {
403 | //manual reset
404 | index = 0;
405 | ctrl.tableState().sort = {};
406 | ctrl.tableState().pagination.start = 0;
407 | func = ctrl.pipe.bind(ctrl);
408 | } else {
409 | func = ctrl.sortBy.bind(ctrl, predicate, index % 2 === 0);
410 | }
411 | if (promise !== null) {
412 | $timeout.cancel(promise);
413 | }
414 | if (throttle < 0) {
415 | func();
416 | } else {
417 | promise = $timeout(function(){
418 | func();
419 | }, throttle);
420 | }
421 | }
422 |
423 | element.bind('click', function sortClick () {
424 | if (predicate) {
425 | scope.$apply(sort);
426 | }
427 | });
428 |
429 | if (sortDefault) {
430 | index = sortDefault === 'reverse' ? 1 : 0;
431 | sort();
432 | }
433 |
434 | //table state --> view
435 | scope.$watch(function () {
436 | return ctrl.tableState().sort;
437 | }, function (newValue) {
438 | if (newValue.predicate !== predicate) {
439 | index = 0;
440 | element
441 | .removeClass(classAscent)
442 | .removeClass(classDescent)
443 | .attr(ariaSort, ariaSortNone);
444 | } else {
445 | index = newValue.reverse === true ? 2 : 1;
446 | element
447 | .removeClass(stateClasses[index % 2])
448 | .addClass(stateClasses[index - 1])
449 | .attr(ariaSort, newValue.reverse ? ariaSortAscending : ariaSortDescending);
450 | }
451 | }, true);
452 | }
453 | };
454 | }]);
455 |
456 | ng.module('smart-table')
457 | .directive('stPagination', ['stConfig', function (stConfig) {
458 | return {
459 | restrict: 'EA',
460 | require: '^stTable',
461 | scope: {
462 | stItemsByPage: '=?',
463 | stDisplayedPages: '=?',
464 | stPageChange: '&'
465 | },
466 | templateUrl: function (element, attrs) {
467 | if (attrs.stTemplate) {
468 | return attrs.stTemplate;
469 | }
470 | return stConfig.pagination.template;
471 | },
472 | link: function (scope, element, attrs, ctrl) {
473 |
474 | scope.stItemsByPage = scope.stItemsByPage ? +(scope.stItemsByPage) : stConfig.pagination.itemsByPage;
475 | scope.stDisplayedPages = scope.stDisplayedPages ? +(scope.stDisplayedPages) : stConfig.pagination.displayedPages;
476 |
477 | scope.currentPage = 1;
478 | scope.pages = [];
479 |
480 | function redraw () {
481 | var paginationState = ctrl.tableState().pagination;
482 | var start = 1;
483 | var end;
484 | var i;
485 | var prevPage = scope.currentPage;
486 | scope.totalItemCount = paginationState.totalItemCount;
487 | scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;
488 |
489 | start = Math.max(start, scope.currentPage - Math.abs(Math.floor(scope.stDisplayedPages / 2)));
490 | end = start + scope.stDisplayedPages;
491 |
492 | if (end > paginationState.numberOfPages) {
493 | end = paginationState.numberOfPages + 1;
494 | start = Math.max(1, end - scope.stDisplayedPages);
495 | }
496 |
497 | scope.pages = [];
498 | scope.numPages = paginationState.numberOfPages;
499 |
500 | for (i = start; i < end; i++) {
501 | scope.pages.push(i);
502 | }
503 |
504 | if (prevPage !== scope.currentPage) {
505 | scope.stPageChange({newPage: scope.currentPage});
506 | }
507 | }
508 |
509 | //table state --> view
510 | scope.$watch(function () {
511 | return ctrl.tableState().pagination;
512 | }, redraw, true);
513 |
514 | //scope --> table state (--> view)
515 | scope.$watch('stItemsByPage', function (newValue, oldValue) {
516 | if (newValue !== oldValue) {
517 | scope.selectPage(1);
518 | }
519 | });
520 |
521 | scope.$watch('stDisplayedPages', redraw);
522 |
523 | //view -> table state
524 | scope.selectPage = function (page) {
525 | if (page > 0 && page <= scope.numPages) {
526 | ctrl.slice((page - 1) * scope.stItemsByPage, scope.stItemsByPage);
527 | }
528 | };
529 |
530 | if (!ctrl.tableState().pagination.number) {
531 | ctrl.slice(0, scope.stItemsByPage);
532 | }
533 | }
534 | };
535 | }]);
536 |
537 | ng.module('smart-table')
538 | .directive('stPipe', ['stConfig', '$timeout', function (config, $timeout) {
539 | return {
540 | require: 'stTable',
541 | scope: {
542 | stPipe: '='
543 | },
544 | link: {
545 |
546 | pre: function (scope, element, attrs, ctrl) {
547 |
548 | var pipePromise = null;
549 |
550 | if (ng.isFunction(scope.stPipe)) {
551 | ctrl.preventPipeOnWatch();
552 | ctrl.pipe = function () {
553 |
554 | if (pipePromise !== null) {
555 | $timeout.cancel(pipePromise)
556 | }
557 |
558 | pipePromise = $timeout(function () {
559 | scope.stPipe(ctrl.tableState(), ctrl);
560 | }, config.pipe.delay);
561 |
562 | return pipePromise;
563 | }
564 | }
565 | },
566 |
567 | post: function (scope, element, attrs, ctrl) {
568 | ctrl.pipe();
569 | }
570 | }
571 | };
572 | }]);
573 |
574 | })(angular);
--------------------------------------------------------------------------------
/test/spec/stSort.spec.js:
--------------------------------------------------------------------------------
1 | describe('stSort Directive', function () {
2 |
3 | var rootScope;
4 | var scope;
5 | var element;
6 | var tableState;
7 |
8 | function hasClass (element, classname) {
9 | return Array.prototype.indexOf.call(element.classList, classname) !== -1
10 | }
11 |
12 | function hasAttr (element, attr, value) {
13 | if (!attr || !value) return false;
14 | return element.getAttribute(attr) === value;
15 | }
16 |
17 | function trToModel (trs) {
18 | return Array.prototype.map.call(trs, function (ele) {
19 | return {
20 | name: ele.cells[0].innerHTML,
21 | firstname: ele.cells[1].innerHTML,
22 | age: +(ele.cells[2].innerHTML)
23 | };
24 | });
25 | }
26 |
27 | //expose table state for tests
28 | beforeEach(module('smart-table', function ($compileProvider) {
29 | $compileProvider.directive('dummy', function () {
30 | return {
31 | restrict: 'A',
32 | require: 'stTable',
33 | link: function (scope, element, attr, ctrl) {
34 | tableState = ctrl.tableState();
35 | }
36 | };
37 | });
38 | }));
39 |
40 | describe('customized stConfig', function () {
41 |
42 | beforeEach(inject(function ($compile, $rootScope, stConfig) {
43 | var oldAscentClass = stConfig.sort.ascentClass;
44 | var oldDescentClass = stConfig.sort.descentClass;
45 | var oldSkipNatural = stConfig.sort.skipNatural;
46 | stConfig.sort.ascentClass = 'custom-ascent';
47 | stConfig.sort.descentClass = 'custom-descent';
48 | stConfig.sort.skipNatural = true;
49 |
50 | rootScope = $rootScope;
51 | scope = $rootScope.$new();
52 | scope.rowCollection = [
53 | {name: 'Renard', firstname: 'Laurent', age: 66},
54 | {name: 'Francoise', firstname: 'Frere', age: 99},
55 | {name: 'Renard', firstname: 'Olivier', age: 33},
56 | {name: 'Leponge', firstname: 'Bob', age: 22},
57 | {name: 'Faivre', firstname: 'Blandine', age: 44}
58 | ];
59 | scope.getters = {
60 | age: function ageGetter (row) {
61 | return row.name.length;
62 | },
63 | name: function nameGetter (row) {
64 | return row.name.length;
65 | }
66 | };
67 |
68 | var template = '' +
69 | '' +
70 | 'name ' +
71 | 'firstname ' +
72 | 'age ' +
73 | 'age ' +
74 | 'age ' +
75 | ' ' +
76 | ' ' +
77 | '' +
78 | '' +
79 | '{{row.name}} ' +
80 | '{{row.firstname}} ' +
81 | '{{row.age}} ' +
82 | ' ' +
83 | ' ' +
84 | '
';
85 |
86 | element = $compile(template)(scope);
87 | scope.$apply();
88 |
89 |
90 | stConfig.sort.ascentClass = oldAscentClass;
91 | stConfig.sort.descentClass = oldDescentClass;
92 | stConfig.sort.skipNatural = oldSkipNatural;
93 | }));
94 |
95 | it('should customize classes for sorting', inject(function ($timeout) {
96 | var ths = element.find('th');
97 | angular.element(ths[1]).triggerHandler('click');
98 | $timeout.flush();
99 | expect(hasClass(ths[1], 'custom-ascent')).toBe(true);
100 | expect(hasClass(ths[1], 'custom-descent')).toBe(false);
101 | }));
102 |
103 | it('should skip natural order', inject(function ($timeout) {
104 | var ths = element.find('th');
105 | var th1 = angular.element(ths[1]);
106 | th1.triggerHandler('click');
107 | th1.triggerHandler('click');
108 | th1.triggerHandler('click');
109 | $timeout.flush();
110 | var actual = trToModel(element.find('tr.test-row'));
111 | expect(hasClass(ths[1], 'custom-ascent')).toBe(true);
112 | expect(hasClass(ths[1], 'custom-descent')).toBe(false);
113 | expect(actual).toEqual([
114 | {name: 'Faivre', firstname: 'Blandine', age: 44},
115 | {name: 'Leponge', firstname: 'Bob', age: 22},
116 | {name: 'Francoise', firstname: 'Frere', age: 99},
117 | {name: 'Renard', firstname: 'Laurent', age: 66},
118 | {name: 'Renard', firstname: 'Olivier', age: 33}
119 | ]);
120 | }));
121 |
122 | it('should sort properly with array value', inject(function ($timeout) {
123 | var ths = element.find('th');
124 | var th4 = angular.element(ths[4]);
125 | th4.triggerHandler('click');
126 | th4.triggerHandler('click');
127 | $timeout.flush();
128 | var actual = trToModel(element.find('tr.test-row'));
129 | expect(actual).toEqual([
130 | {name: 'Renard', firstname: 'Laurent', age: 66},
131 | {name: 'Renard', firstname: 'Olivier', age: 33},
132 | {name: 'Leponge', firstname: 'Bob', age: 22},
133 | {name: 'Francoise', firstname: 'Frere', age: 99},
134 | {name: 'Faivre', firstname: 'Blandine', age: 44},
135 | ]);
136 | }));
137 |
138 | });
139 |
140 | describe('normal stConfig', function () {
141 |
142 | beforeEach(inject(function ($compile, $rootScope) {
143 |
144 | rootScope = $rootScope;
145 | scope = $rootScope.$new();
146 | scope.rowCollection = [
147 | {name: 'Renard', firstname: 'Laurent', age: 66},
148 | {name: 'Francoise', firstname: 'Frere', age: 99},
149 | {name: 'Renard', firstname: 'Olivier', age: 33},
150 | {name: 'Leponge', firstname: 'Bob', age: 22},
151 | {name: 'Faivre', firstname: 'Blandine', age: 44}
152 | ];
153 | scope.getters = {
154 | age: function ageGetter (row) {
155 | return row.name.length;
156 | },
157 | name: function nameGetter (row) {
158 | return row.name.length;
159 | }
160 | };
161 |
162 | var template = '' +
163 | '' +
164 | 'name ' +
165 | 'firstname ' +
166 | 'age ' +
167 | 'age ' +
168 | ' ' +
169 | ' ' +
170 | '' +
171 | '' +
172 | '{{row.name}} ' +
173 | '{{row.firstname}} ' +
174 | '{{row.age}} ' +
175 | ' ' +
176 | ' ' +
177 | '
';
178 |
179 | element = $compile(template)(scope);
180 | scope.$apply();
181 | }));
182 |
183 | it('should sort by clicked header', inject(function ($timeout) {
184 | var ths = element.find('th');
185 | var actual;
186 | angular.element(ths[1]).triggerHandler('click');
187 | $timeout.flush();
188 | actual = trToModel(element.find('tr.test-row'));
189 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(true);
190 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
191 | expect(actual).toEqual([
192 | {name: 'Faivre', firstname: 'Blandine', age: 44},
193 | {name: 'Leponge', firstname: 'Bob', age: 22},
194 | {name: 'Francoise', firstname: 'Frere', age: 99},
195 | {name: 'Renard', firstname: 'Laurent', age: 66},
196 | {name: 'Renard', firstname: 'Olivier', age: 33}
197 | ]);
198 | }));
199 |
200 | it('should revert on the second click', inject(function ($timeout) {
201 | var ths = element.find('th');
202 | var actual;
203 | angular.element(ths[1]).triggerHandler('click');
204 | $timeout.flush();
205 | angular.element(ths[1]).triggerHandler('click');
206 | $timeout.flush();
207 | actual = trToModel(element.find('tr.test-row'));
208 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
209 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(true);
210 | expect(actual).toEqual([
211 | {name: 'Renard', firstname: 'Olivier', age: 33},
212 | {name: 'Renard', firstname: 'Laurent', age: 66},
213 | {name: 'Francoise', firstname: 'Frere', age: 99},
214 | {name: 'Leponge', firstname: 'Bob', age: 22},
215 | {name: 'Faivre', firstname: 'Blandine', age: 44}
216 | ]);
217 | }));
218 |
219 | it('should reset the sort state on the third call', inject(function ($timeout) {
220 | var ths = element.find('th');
221 | var actual;
222 | angular.element(ths[1]).triggerHandler('click');
223 | $timeout.flush();
224 | angular.element(ths[1]).triggerHandler('click');
225 | $timeout.flush();
226 | tableState.sort = {
227 | predicate: 'firstname',
228 | reverse: true
229 | };
230 | tableState.pagination.start = 40;
231 | angular.element(ths[1]).triggerHandler('click');
232 | $timeout.flush();
233 | actual = trToModel(element.find('tr.test-row'));
234 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
235 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
236 | expect(actual).toEqual([
237 | {name: 'Renard', firstname: 'Laurent', age: 66},
238 | {name: 'Francoise', firstname: 'Frere', age: 99},
239 | {name: 'Renard', firstname: 'Olivier', age: 33},
240 | {name: 'Leponge', firstname: 'Bob', age: 22},
241 | {name: 'Faivre', firstname: 'Blandine', age: 44}
242 | ]);
243 | expect(tableState.sort).toEqual({});
244 | expect(tableState.pagination.start).toEqual(0);
245 | }));
246 |
247 | it('should support getter function as predicate', inject(function ($timeout) {
248 | var ths = element.find('th');
249 | var actual;
250 | angular.element(ths[2]).triggerHandler('click');
251 | $timeout.flush();
252 | actual = trToModel(element.find('tr.test-row'));
253 | expect(actual).toEqual([
254 | {name: 'Renard', firstname: 'Laurent', age: 66},
255 | {name: 'Renard', firstname: 'Olivier', age: 33},
256 | {name: 'Faivre', firstname: 'Blandine', age: 44},
257 | {name: 'Leponge', firstname: 'Bob', age: 22},
258 | {name: 'Francoise', firstname: 'Frere', age: 99}
259 | ]);
260 | }));
261 |
262 | it('should switch from getter function to the other', inject(function ($timeout) {
263 | var ths = element.find('th');
264 | var actual;
265 | angular.element(ths[2]).triggerHandler('click');
266 | $timeout.flush();
267 | expect(hasClass(ths[2], 'st-sort-ascent')).toBe(true);
268 | expect(hasClass(ths[3], 'st-sort-ascent')).toBe(false);
269 |
270 | angular.element(ths[3]).triggerHandler('click');
271 | $timeout.flush();
272 | expect(hasClass(ths[2], 'st-sort-ascent')).toBe(false);
273 | expect(hasClass(ths[3], 'st-sort-ascent')).toBe(true);
274 | }));
275 |
276 | it('should reset its class if table state has changed', inject(function ($timeout) {
277 | var ths = element.find('th');
278 | angular.element(ths[1]).triggerHandler('click');
279 | $timeout.flush();
280 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(true);
281 |
282 | tableState.sort = {
283 | predicate: 'lastname'
284 | };
285 |
286 | scope.$apply();
287 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
288 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
289 |
290 | }));
291 |
292 | it('should sort by default a column', inject(function ($compile, $timeout) {
293 | var template = '' +
294 | '' +
295 | 'name ' +
296 | 'firstname ' +
297 | 'age ' +
298 | ' ' +
299 | ' ' +
300 | '' +
301 | '' +
302 | '{{row.name}} ' +
303 | '{{row.firstname}} ' +
304 | '{{row.age}} ' +
305 | ' ' +
306 | ' ' +
307 | '
';
308 |
309 | element = $compile(template)(scope);
310 |
311 | $timeout.flush();
312 |
313 | var ths = element.find('th');
314 | var actual = trToModel(element.find('tr.test-row'));
315 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(true);
316 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
317 | expect(actual).toEqual([
318 | {name: 'Faivre', firstname: 'Blandine', age: 44},
319 | {name: 'Leponge', firstname: 'Bob', age: 22},
320 | {name: 'Francoise', firstname: 'Frere', age: 99},
321 | {name: 'Renard', firstname: 'Laurent', age: 66},
322 | {name: 'Renard', firstname: 'Olivier', age: 33}
323 | ]);
324 | }));
325 |
326 | it('should evaluate st sort default and consider a falsy value', inject(function ($compile) {
327 |
328 | scope.column = {reverse: false};
329 |
330 | var template = '' +
331 | '' +
332 | 'name ' +
333 | 'firstname ' +
334 | 'age ' +
335 | ' ' +
336 | ' ' +
337 | '' +
338 | '' +
339 | '{{row.name}} ' +
340 | '{{row.firstname}} ' +
341 | '{{row.age}} ' +
342 | ' ' +
343 | ' ' +
344 | '
';
345 |
346 | element = $compile(template)(scope);
347 |
348 | scope.$apply();
349 |
350 | var ths = element.find('th');
351 | var actual = trToModel(element.find('tr.test-row'));
352 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
353 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
354 | expect(actual).toEqual([
355 | {name: 'Renard', firstname: 'Laurent', age: 66},
356 | {name: 'Francoise', firstname: 'Frere', age: 99},
357 | {name: 'Renard', firstname: 'Olivier', age: 33},
358 | {name: 'Leponge', firstname: 'Bob', age: 22},
359 | {name: 'Faivre', firstname: 'Blandine', age: 44}
360 | ]);
361 |
362 | }));
363 |
364 | it('should sort by default a column in reverse mode', inject(function ($compile, $timeout) {
365 | var template = '' +
366 | '' +
367 | 'name ' +
368 | 'firstname ' +
369 | 'age ' +
370 | ' ' +
371 | ' ' +
372 | '' +
373 | '' +
374 | '{{row.name}} ' +
375 | '{{row.firstname}} ' +
376 | '{{row.age}} ' +
377 | ' ' +
378 | ' ' +
379 | '
';
380 |
381 | element = $compile(template)(scope);
382 |
383 | $timeout.flush();
384 |
385 | var ths = element.find('th');
386 | var actual = trToModel(element.find('tr.test-row'));
387 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
388 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(true);
389 | expect(actual).toEqual([
390 | {name: 'Renard', firstname: 'Olivier', age: 33},
391 | {name: 'Renard', firstname: 'Laurent', age: 66},
392 | {name: 'Francoise', firstname: 'Frere', age: 99},
393 | {name: 'Leponge', firstname: 'Bob', age: 22},
394 | {name: 'Faivre', firstname: 'Blandine', age: 44}
395 | ]);
396 | }));
397 |
398 |
399 | it('should skip natural order', inject(function ($compile, $timeout) {
400 | var template = '' +
401 | '' +
402 | 'name ' +
403 | 'firstname ' +
404 | 'age ' +
405 | ' ' +
406 | ' ' +
407 | '' +
408 | '' +
409 | '{{row.name}} ' +
410 | '{{row.firstname}} ' +
411 | '{{row.age}} ' +
412 | ' ' +
413 | ' ' +
414 | '
';
415 |
416 | element = $compile(template)(scope);
417 |
418 | scope.$apply();
419 |
420 | var ths = element.find('th');
421 | var th1 = angular.element(ths[1]);
422 | th1.triggerHandler('click');
423 | $timeout.flush();
424 | th1.triggerHandler('click');
425 | $timeout.flush();
426 | th1.triggerHandler('click');
427 | $timeout.flush();
428 | var actual = trToModel(element.find('tr.test-row'));
429 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(true);
430 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
431 | expect(actual).toEqual([
432 | {name: 'Faivre', firstname: 'Blandine', age: 44},
433 | {name: 'Leponge', firstname: 'Bob', age: 22},
434 | {name: 'Francoise', firstname: 'Frere', age: 99},
435 | {name: 'Renard', firstname: 'Laurent', age: 66},
436 | {name: 'Renard', firstname: 'Olivier', age: 33}
437 | ]);
438 | }));
439 |
440 | it('should sort by clicked header in descending order first (by default) when requested', inject(function ($timeout, $compile, stConfig) {
441 | var template = '' +
442 | '' +
443 | 'name ' +
444 | 'firstname ' +
445 | 'age ' +
446 | 'age ' +
447 | ' ' +
448 | ' ' +
449 | '' +
450 | '' +
451 | '{{row.name}} ' +
452 | '{{row.firstname}} ' +
453 | '{{row.age}} ' +
454 | ' ' +
455 | ' ' +
456 | '
';
457 |
458 | stConfig.sort.descendingFirst = 'true'; // or any defined value
459 |
460 | element = $compile(template)(scope);
461 | scope.$apply();
462 |
463 | var ths = element.find('th');
464 | var actual;
465 | angular.element(ths[1]).triggerHandler('click');
466 | $timeout.flush();
467 | actual = trToModel(element.find('tr.test-row'));
468 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
469 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(true);
470 | expect(actual).toEqual([
471 | {name: 'Renard', firstname: 'Olivier', age: 33},
472 | {name: 'Renard', firstname: 'Laurent', age: 66},
473 | {name: 'Francoise', firstname: 'Frere', age: 99},
474 | {name: 'Leponge', firstname: 'Bob', age: 22},
475 | {name: 'Faivre', firstname: 'Blandine', age: 44}
476 | ]);
477 | }));
478 |
479 | it('should sort by clicked header in descending order first when requested', inject(function ($timeout, $compile) {
480 | var template = '' +
481 | '' +
482 | 'name ' +
483 | 'firstname ' +
484 | 'age ' +
485 | 'age ' +
486 | ' ' +
487 | ' ' +
488 | '' +
489 | '' +
490 | '{{row.name}} ' +
491 | '{{row.firstname}} ' +
492 | '{{row.age}} ' +
493 | ' ' +
494 | ' ' +
495 | '
';
496 |
497 | element = $compile(template)(scope);
498 | scope.$apply();
499 |
500 | var ths = element.find('th');
501 | var actual;
502 | angular.element(ths[1]).triggerHandler('click');
503 | $timeout.flush();
504 | actual = trToModel(element.find('tr.test-row'));
505 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
506 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(true);
507 | expect(actual).toEqual([
508 | {name: 'Renard', firstname: 'Olivier', age: 33},
509 | {name: 'Renard', firstname: 'Laurent', age: 66},
510 | {name: 'Francoise', firstname: 'Frere', age: 99},
511 | {name: 'Leponge', firstname: 'Bob', age: 22},
512 | {name: 'Faivre', firstname: 'Blandine', age: 44}
513 | ]);
514 | }));
515 |
516 | it('should switch to ascending order on the second click', inject(function ($timeout, $compile) {
517 | var template = '' +
518 | '' +
519 | 'name ' +
520 | 'firstname ' +
521 | 'age ' +
522 | 'age ' +
523 | ' ' +
524 | ' ' +
525 | '' +
526 | '' +
527 | '{{row.name}} ' +
528 | '{{row.firstname}} ' +
529 | '{{row.age}} ' +
530 | ' ' +
531 | ' ' +
532 | '
';
533 |
534 | element = $compile(template)(scope);
535 | scope.$apply();
536 |
537 | var ths = element.find('th');
538 | var actual;
539 | angular.element(ths[1]).triggerHandler('click');
540 | $timeout.flush();
541 | angular.element(ths[1]).triggerHandler('click');
542 | $timeout.flush();
543 | actual = trToModel(element.find('tr.test-row'));
544 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(true);
545 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
546 | expect(actual).toEqual([
547 | {name: 'Faivre', firstname: 'Blandine', age: 44},
548 | {name: 'Leponge', firstname: 'Bob', age: 22},
549 | {name: 'Francoise', firstname: 'Frere', age: 99},
550 | {name: 'Renard', firstname: 'Laurent', age: 66},
551 | {name: 'Renard', firstname: 'Olivier', age: 33}
552 | ]);
553 | }));
554 |
555 | it('should reset the sort state on the third call regardless of st-desending-first', inject(function ($timeout, $compile) {
556 | var template = '' +
557 | '' +
558 | 'name ' +
559 | 'firstname ' +
560 | 'age ' +
561 | 'age ' +
562 | ' ' +
563 | ' ' +
564 | '' +
565 | '' +
566 | '{{row.name}} ' +
567 | '{{row.firstname}} ' +
568 | '{{row.age}} ' +
569 | ' ' +
570 | ' ' +
571 | '
';
572 |
573 | element = $compile(template)(scope);
574 | scope.$apply();
575 |
576 | var ths = element.find('th');
577 | var actual;
578 | angular.element(ths[1]).triggerHandler('click');
579 | $timeout.flush();
580 | angular.element(ths[1]).triggerHandler('click');
581 | $timeout.flush();
582 | tableState.sort = {
583 | predicate: 'firstname',
584 | reverse: true
585 | };
586 | tableState.pagination.start = 40;
587 | angular.element(ths[1]).triggerHandler('click');
588 | $timeout.flush();
589 | actual = trToModel(element.find('tr.test-row'));
590 | expect(hasClass(ths[1], 'st-sort-ascent')).toBe(false);
591 | expect(hasClass(ths[1], 'st-sort-descent')).toBe(false);
592 | expect(actual).toEqual([
593 | {name: 'Renard', firstname: 'Laurent', age: 66},
594 | {name: 'Francoise', firstname: 'Frere', age: 99},
595 | {name: 'Renard', firstname: 'Olivier', age: 33},
596 | {name: 'Leponge', firstname: 'Bob', age: 22},
597 | {name: 'Faivre', firstname: 'Blandine', age: 44}
598 | ]);
599 | expect(tableState.sort).toEqual({});
600 | expect(tableState.pagination.start).toEqual(0);
601 | }));
602 |
603 | it('should initialize headers with the aria role attribute set to columnheader', inject(function ($timeout) {
604 | var ths = element.find('th');
605 | expect((ths[1], ''))
606 | expect(hasAttr(ths[1], 'role', 'columnheader')).toBe(true);
607 | }));
608 |
609 | it('should update aria-sort attribute when clicking', inject(function ($timeout) {
610 | var ariaSort = 'aria-sort';
611 | var ariaSortNone = 'none';
612 | var ariaSortAscending = 'ascending';
613 | var ariaSortDescending = 'descending';
614 |
615 | var ths = element.find('th');
616 | expect(hasAttr(ths[1], ariaSort, ariaSortNone)).toBe(true);
617 | angular.element(ths[1]).triggerHandler('click');
618 | $timeout.flush();
619 | expect(hasAttr(ths[1], ariaSort, ariaSortAscending)).toBe(true);
620 | angular.element(ths[1]).triggerHandler('click');
621 | $timeout.flush();
622 | expect(hasAttr(ths[1], ariaSort, ariaSortDescending)).toBe(true);
623 | angular.element(ths[1]).triggerHandler('click');
624 | $timeout.flush();
625 | expect(hasAttr(ths[1], ariaSort, ariaSortNone)).toBe(true);
626 | }));
627 |
628 |
629 | });
630 |
631 |
632 | });
633 |
--------------------------------------------------------------------------------
/dist/smart-table.min.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["smart-table.min.js"],"names":["ng","undefined","module","run","$templateCache","put","constant","pagination","template","itemsByPage","displayedPages","search","delay","inputEvent","trimSearch","select","mode","selectedClass","sort","ascentClass","descentClass","descendingFirst","skipNatural","pipe","controller","$scope","$parse","$filter","$attrs","copyRefs","src","concat","updateSafeCopy","safeCopy","safeGetter","pipeAfterSafeCopy","ctrl","deepDelete","object","path","indexOf","partials","split","key","pop","parentPath","join","parentObject","Object","keys","length","filtered","lastSelected","propertyName","stTable","displayGetter","displaySetter","assign","orderBy","filter","tableState","start","totalItemCount","this","stSafeSrc","$watch","safeSrc","newValue","oldValue","sortBy","predicate","reverse","isFunction","functionName","name","input","comparator","predicateObject","prop","output","number","numberOfPages","Math","ceil","slice","parseInt","row","rows","index","isSelected","getFilteredCollection","setFilterFunction","filterName","setSortFunction","sortFunctionName","preventPipeOnWatch","directive","restrict","link","scope","element","attr","stSetFilter","stSetSort","stConfig","$timeout","require","tableCtrl","promise","throttle","stDelay","event","stInputEvent","$observe","value","isString","trim","predicateExpression","stSearch","bind","evt","originalEvent","cancel","target","stSelectMode","$apply","addClass","removeClass","func","getter","isArray","stSort","sortDefault","classAscent","stClassAscent","classDescent","stClassDescent","stateClasses","stSkipNatural","stDescendingFirst","ariaSort","stSortDefault","$eval","stItemsByPage","stDisplayedPages","stPageChange","templateUrl","attrs","stTemplate","redraw","end","i","paginationState","prevPage","currentPage","floor","max","abs","pages","numPages","push","newPage","selectPage","page","config","stPipe","pre","pipePromise","post","angular"],"mappings":"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","file":"smart-table.min.js","sourcesContent":["(function (ng, undefined){\n 'use strict';\n\nng.module('smart-table', []).run(['$templateCache', function ($templateCache) {\n $templateCache.put('template/smart-table/pagination.html',\n '= 2\"> ');\n}]);\n\n\nng.module('smart-table')\n .constant('stConfig', {\n pagination: {\n template: 'template/smart-table/pagination.html',\n itemsByPage: 10,\n displayedPages: 5\n },\n search: {\n delay: 400, // ms\n inputEvent: 'input',\n trimSearch: false\n },\n select: {\n mode: 'single',\n selectedClass: 'st-selected'\n },\n sort: {\n ascentClass: 'st-sort-ascent',\n descentClass: 'st-sort-descent',\n descendingFirst: false,\n skipNatural: false,\n delay:300\n },\n pipe: {\n delay: 100 //ms\n }\n });\nng.module('smart-table').controller('stTableController', [\n '$scope',\n '$parse',\n '$filter',\n '$attrs',\n function StTableController($scope, $parse, $filter, $attrs) {\n var propertyName = $attrs.stTable;\n var displayGetter = $parse(propertyName);\n var displaySetter = displayGetter.assign;\n var safeGetter;\n var orderBy = $filter('orderBy');\n var filter = $filter('filter');\n var safeCopy = copyRefs(displayGetter($scope));\n var tableState = {\n sort: {},\n search: {},\n pagination: { start: 0, totalItemCount: 0 }\n };\n var filtered;\n var pipeAfterSafeCopy = true;\n var ctrl = this;\n var lastSelected;\n\n function copyRefs(src) {\n return src ? [].concat(src) : [];\n }\n\n function updateSafeCopy() {\n safeCopy = copyRefs(safeGetter($scope));\n if (pipeAfterSafeCopy === true) {\n ctrl.pipe();\n }\n }\n\n function deepDelete(object, path) {\n if (path.indexOf('.') != -1) {\n var partials = path.split('.');\n var key = partials.pop();\n var parentPath = partials.join('.');\n var parentObject = $parse(parentPath)(object);\n delete parentObject[key];\n if (Object.keys(parentObject).length == 0) {\n deepDelete(object, parentPath);\n }\n } else {\n delete object[path];\n }\n }\n\n if ($attrs.stSafeSrc) {\n safeGetter = $parse($attrs.stSafeSrc);\n $scope.$watch(\n function() {\n var safeSrc = safeGetter($scope);\n return safeSrc && safeSrc.length ? safeSrc[0] : undefined;\n },\n function(newValue, oldValue) {\n if (newValue !== oldValue) {\n updateSafeCopy();\n }\n }\n );\n $scope.$watch(\n function() {\n var safeSrc = safeGetter($scope);\n return safeSrc ? safeSrc.length : 0;\n },\n function(newValue, oldValue) {\n if (newValue !== safeCopy.length) {\n updateSafeCopy();\n }\n }\n );\n $scope.$watch(\n function() {\n return safeGetter($scope);\n },\n function(newValue, oldValue) {\n if (newValue !== oldValue) {\n tableState.pagination.start = 0;\n updateSafeCopy();\n }\n }\n );\n }\n\n /**\n * sort the rows\n * @param {Function | String} predicate - function or string which will be used as predicate for the sorting\n * @param [reverse] - if you want to reverse the order\n */\n this.sortBy = function sortBy(predicate, reverse) {\n tableState.sort.predicate = predicate;\n tableState.sort.reverse = reverse === true;\n\n if (ng.isFunction(predicate)) {\n tableState.sort.functionName = predicate.name;\n } else {\n delete tableState.sort.functionName;\n }\n\n tableState.pagination.start = 0;\n return this.pipe();\n };\n\n /**\n * search matching rows\n * @param {String} input - the input string\n * @param {String} [predicate] - the property name against you want to check the match, otherwise it will search on all properties\n * @param {String | Function } [comparator] - a comparator to pass to the filter for the (pass true for stric mode)\n */\n this.search = function search(input, predicate, comparator) {\n var predicateObject = tableState.search.predicateObject || {};\n var prop = predicate ? predicate : '$';\n\n $parse(prop).assign(predicateObject, input);\n // to avoid to filter out null value\n if (!input) {\n deepDelete(predicateObject, prop);\n }\n tableState.search.predicateObject = predicateObject;\n tableState.pagination.start = 0;\n return this.pipe();\n };\n\n /**\n * this will chain the operations of sorting and filtering based on the current table state (sort options, filtering, ect)\n */\n this.pipe = function pipe() {\n var pagination = tableState.pagination;\n var output;\n filtered = tableState.search.predicateObject\n ? filter(safeCopy, tableState.search.predicateObject)\n : safeCopy;\n if (tableState.sort.predicate) {\n filtered = orderBy(\n filtered,\n tableState.sort.predicate,\n tableState.sort.reverse\n );\n }\n pagination.totalItemCount = filtered.length;\n if (pagination.number !== undefined) {\n pagination.numberOfPages = filtered.length > 0\n ? Math.ceil(filtered.length / pagination.number)\n : 1;\n pagination.start = pagination.start >= filtered.length\n ? (pagination.numberOfPages - 1) * pagination.number\n : pagination.start;\n output = filtered.slice(\n pagination.start,\n pagination.start + parseInt(pagination.number)\n );\n }\n displaySetter($scope, output || filtered);\n };\n\n /**\n * select a dataRow (it will add the attribute isSelected to the row object)\n * @param {Object} row - the row to select\n * @param {String} [mode] - \"single\" or \"multiple\" (multiple by default)\n */\n this.select = function select(row, mode) {\n var rows = copyRefs(displayGetter($scope));\n var index = rows.indexOf(row);\n if (index !== -1) {\n if (mode === 'single') {\n row.isSelected = row.isSelected !== true;\n if (lastSelected) {\n lastSelected.isSelected = false;\n }\n lastSelected = row.isSelected === true ? row : undefined;\n } else {\n rows[index].isSelected = !rows[index].isSelected;\n }\n }\n };\n\n /**\n * take a slice of the current sorted/filtered collection (pagination)\n *\n * @param {Number} start - start index of the slice\n * @param {Number} number - the number of item in the slice\n */\n this.slice = function splice(start, number) {\n tableState.pagination.start = start;\n tableState.pagination.number = number;\n return this.pipe();\n };\n\n /**\n * return the current state of the table\n * @returns {{sort: {}, search: {}, pagination: {start: number}}}\n */\n this.tableState = function getTableState() {\n return tableState;\n };\n\n this.getFilteredCollection = function getFilteredCollection() {\n return filtered || safeCopy;\n };\n\n /**\n * Use a different filter function than the angular FilterFilter\n * @param filterName the name under which the custom filter is registered\n */\n this.setFilterFunction = function setFilterFunction(filterName) {\n filter = $filter(filterName);\n };\n\n /**\n * Use a different function than the angular orderBy\n * @param sortFunctionName the name under which the custom order function is registered\n */\n this.setSortFunction = function setSortFunction(sortFunctionName) {\n orderBy = $filter(sortFunctionName);\n };\n\n /**\n * Usually when the safe copy is updated the pipe function is called.\n * Calling this method will prevent it, which is something required when using a custom pipe function\n */\n this.preventPipeOnWatch = function preventPipe() {\n pipeAfterSafeCopy = false;\n };\n }\n]).directive('stTable', function() {\n return {\n restrict: 'A',\n controller: 'stTableController',\n link: function(scope, element, attr, ctrl) {\n if (attr.stSetFilter) {\n ctrl.setFilterFunction(attr.stSetFilter);\n }\n\n if (attr.stSetSort) {\n ctrl.setSortFunction(attr.stSetSort);\n }\n }\n };\n});\n\nng.module('smart-table')\n .directive('stSearch', ['stConfig', '$timeout','$parse', function (stConfig, $timeout, $parse) {\n return {\n require: '^stTable',\n link: function (scope, element, attr, ctrl) {\n var tableCtrl = ctrl;\n var promise = null;\n var throttle = attr.stDelay || stConfig.search.delay;\n var event = attr.stInputEvent || stConfig.search.inputEvent;\n var trimSearch = attr.trimSearch || stConfig.search.trimSearch;\n\n attr.$observe('stSearch', function (newValue, oldValue) {\n var input = element[0].value;\n if (newValue !== oldValue && input) {\n ctrl.tableState().search = {};\n input = ng.isString(input) && trimSearch ? input.trim() : input;\n tableCtrl.search(input, newValue);\n }\n });\n\n //table state -> view\n scope.$watch(function () {\n return ctrl.tableState().search;\n }, function (newValue, oldValue) {\n var predicateExpression = attr.stSearch || '$';\n if (newValue.predicateObject && $parse(predicateExpression)(newValue.predicateObject) !== element[0].value) {\n element[0].value = $parse(predicateExpression)(newValue.predicateObject) || '';\n }\n }, true);\n\n // view -> table state\n element.bind(event, function (evt) {\n evt = evt.originalEvent || evt;\n if (promise !== null) {\n $timeout.cancel(promise);\n }\n\n promise = $timeout(function () {\n var input = evt.target.value;\n input = ng.isString(input) && trimSearch ? input.trim() : input;\n tableCtrl.search(input, attr.stSearch || '');\n promise = null;\n }, throttle);\n });\n }\n };\n }]);\n\nng.module('smart-table')\n .directive('stSelectRow', ['stConfig', function (stConfig) {\n return {\n restrict: 'A',\n require: '^stTable',\n scope: {\n row: '=stSelectRow'\n },\n link: function (scope, element, attr, ctrl) {\n var mode = attr.stSelectMode || stConfig.select.mode;\n element.bind('click', function () {\n scope.$apply(function () {\n ctrl.select(scope.row, mode);\n });\n });\n\n scope.$watch('row.isSelected', function (newValue) {\n if (newValue === true) {\n element.addClass(stConfig.select.selectedClass);\n } else {\n element.removeClass(stConfig.select.selectedClass);\n }\n });\n }\n };\n }]);\n\nng.module('smart-table')\n .directive('stSort', ['stConfig', '$parse', '$timeout', function (stConfig, $parse, $timeout) {\n return {\n restrict: 'A',\n require: '^stTable',\n link: function (scope, element, attr, ctrl) {\n\n var predicate = attr.stSort;\n var getter = $parse(predicate);\n var index = 0;\n var classAscent = attr.stClassAscent || stConfig.sort.ascentClass;\n var classDescent = attr.stClassDescent || stConfig.sort.descentClass;\n var stateClasses = [classAscent, classDescent];\n var sortDefault;\n var skipNatural = attr.stSkipNatural !== undefined ? attr.stSkipNatural : stConfig.sort.skipNatural;\n var descendingFirst = attr.stDescendingFirst !== undefined ? attr.stDescendingFirst : stConfig.sort.descendingFirst;\n var promise = null;\n var throttle = attr.stDelay || stConfig.sort.delay;\n\n // set aria attributes\n var ariaSort = 'aria-sort';\n var ariaSortNone = 'none';\n var ariaSortAscending = 'ascending';\n var ariaSortDescending = 'descending';\n element\n .attr('role', 'columnheader')\n .attr(ariaSort, ariaSortNone);\n\n if (attr.stSortDefault) {\n sortDefault = scope.$eval(attr.stSortDefault) !== undefined ? scope.$eval(attr.stSortDefault) : attr.stSortDefault;\n }\n\n //view --> table state\n function sort () {\n if (descendingFirst) {\n index = index === 0 ? 2 : index - 1;\n } else {\n index++;\n }\n\n var func;\n predicate = ng.isFunction(getter(scope)) || ng.isArray(getter(scope)) ? getter(scope) : attr.stSort;\n if (index % 3 === 0 && !!skipNatural !== true) {\n //manual reset\n index = 0;\n ctrl.tableState().sort = {};\n ctrl.tableState().pagination.start = 0;\n func = ctrl.pipe.bind(ctrl);\n } else {\n func = ctrl.sortBy.bind(ctrl, predicate, index % 2 === 0);\n }\n if (promise !== null) {\n $timeout.cancel(promise);\n }\n if (throttle < 0) {\n func();\n } else {\n promise = $timeout(function(){\n func();\n }, throttle);\n }\n }\n\n element.bind('click', function sortClick () {\n if (predicate) {\n scope.$apply(sort);\n }\n });\n\n if (sortDefault) {\n index = sortDefault === 'reverse' ? 1 : 0;\n sort();\n }\n\n //table state --> view\n scope.$watch(function () {\n return ctrl.tableState().sort;\n }, function (newValue) {\n if (newValue.predicate !== predicate) {\n index = 0;\n element\n .removeClass(classAscent)\n .removeClass(classDescent)\n .attr(ariaSort, ariaSortNone);\n } else {\n index = newValue.reverse === true ? 2 : 1;\n element\n .removeClass(stateClasses[index % 2])\n .addClass(stateClasses[index - 1])\n .attr(ariaSort, newValue.reverse ? ariaSortAscending : ariaSortDescending);\n }\n }, true);\n }\n };\n }]);\n\nng.module('smart-table')\n .directive('stPagination', ['stConfig', function (stConfig) {\n return {\n restrict: 'EA',\n require: '^stTable',\n scope: {\n stItemsByPage: '=?',\n stDisplayedPages: '=?',\n stPageChange: '&'\n },\n templateUrl: function (element, attrs) {\n if (attrs.stTemplate) {\n return attrs.stTemplate;\n }\n return stConfig.pagination.template;\n },\n link: function (scope, element, attrs, ctrl) {\n\n scope.stItemsByPage = scope.stItemsByPage ? +(scope.stItemsByPage) : stConfig.pagination.itemsByPage;\n scope.stDisplayedPages = scope.stDisplayedPages ? +(scope.stDisplayedPages) : stConfig.pagination.displayedPages;\n\n scope.currentPage = 1;\n scope.pages = [];\n\n function redraw () {\n var paginationState = ctrl.tableState().pagination;\n var start = 1;\n var end;\n var i;\n var prevPage = scope.currentPage;\n scope.totalItemCount = paginationState.totalItemCount;\n scope.currentPage = Math.floor(paginationState.start / paginationState.number) + 1;\n\n start = Math.max(start, scope.currentPage - Math.abs(Math.floor(scope.stDisplayedPages / 2)));\n end = start + scope.stDisplayedPages;\n\n if (end > paginationState.numberOfPages) {\n end = paginationState.numberOfPages + 1;\n start = Math.max(1, end - scope.stDisplayedPages);\n }\n\n scope.pages = [];\n scope.numPages = paginationState.numberOfPages;\n\n for (i = start; i < end; i++) {\n scope.pages.push(i);\n }\n\n if (prevPage !== scope.currentPage) {\n scope.stPageChange({newPage: scope.currentPage});\n }\n }\n\n //table state --> view\n scope.$watch(function () {\n return ctrl.tableState().pagination;\n }, redraw, true);\n\n //scope --> table state (--> view)\n scope.$watch('stItemsByPage', function (newValue, oldValue) {\n if (newValue !== oldValue) {\n scope.selectPage(1);\n }\n });\n\n scope.$watch('stDisplayedPages', redraw);\n\n //view -> table state\n scope.selectPage = function (page) {\n if (page > 0 && page <= scope.numPages) {\n ctrl.slice((page - 1) * scope.stItemsByPage, scope.stItemsByPage);\n }\n };\n\n if (!ctrl.tableState().pagination.number) {\n ctrl.slice(0, scope.stItemsByPage);\n }\n }\n };\n }]);\n\nng.module('smart-table')\n .directive('stPipe', ['stConfig', '$timeout', function (config, $timeout) {\n return {\n require: 'stTable',\n scope: {\n stPipe: '='\n },\n link: {\n\n pre: function (scope, element, attrs, ctrl) {\n\n var pipePromise = null;\n\n if (ng.isFunction(scope.stPipe)) {\n ctrl.preventPipeOnWatch();\n ctrl.pipe = function () {\n\n if (pipePromise !== null) {\n $timeout.cancel(pipePromise)\n }\n\n pipePromise = $timeout(function () {\n scope.stPipe(ctrl.tableState(), ctrl);\n }, config.pipe.delay);\n\n return pipePromise;\n }\n }\n },\n\n post: function (scope, element, attrs, ctrl) {\n ctrl.pipe();\n }\n }\n };\n }]);\n\n})(angular);"]}
--------------------------------------------------------------------------------