;
166 | // If true, will show a number indicating stacked sort priority of each column being sorted.
167 | showSortPriority?: boolean;
168 | // If true, a column's filter state will be removed when that column is hidden.
169 | clearFilterOnColumnHide?: boolean;
170 | // If true, a column's sort state will be removed when that column is hidden.
171 | clearSortOnColumnHide?: boolean;
172 | }
173 | interface IRowScope extends ng.IScope {
174 | toggleRowExpand: Function;
175 | rowIsExpanded: boolean;
176 | refreshExpandedHeight: Function;
177 | row: any;
178 | options: ITableOptions;
179 | }
180 | interface ICellScope extends ng.IScope {
181 | toggleRowExpand: Function;
182 | rowIsExpanded: boolean;
183 | refreshExpandedHeight: Function;
184 | row: any;
185 | column: ITableColumn;
186 | options: ITableOptions;
187 | }
188 | interface ITableStorageState {
189 | sortOrder: IInitialSort[];
190 | searchTerms: { [columnId: string]: string };
191 | enabledColumns: string[];
192 | options: {
193 | rowLimit: number;
194 | pagingStrategy: PAGING_STRATEGY;
195 | storageHash?: string;
196 | };
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/app/.buildignore:
--------------------------------------------------------------------------------
1 | *.coffee
--------------------------------------------------------------------------------
/app/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found :(
6 |
141 |
142 |
143 |
144 |
Not found :(
145 |
Sorry, but the page you were trying to view does not exist.
146 |
It looks like this was the result of either:
147 |
148 | a mistyped address
149 | an out-of-date link
150 |
151 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/favicon.ico
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/app/images/yeoman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/app/images/yeoman.png
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | andyperlitch: angularjs-table
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
40 |
41 |
42 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/app/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/app/scripts/controllers/clickable-rows.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 | .controller('ClickableRowsCtrl', function($scope, $q, phoneData, $templateCache) {
5 |
6 | $scope.my_table_options = {
7 | expandableTemplateUrl: 'views/expandable-panel.html',
8 | bodyHeight: 600,
9 | rowPadding: 600
10 | };
11 | $scope.my_table_columns = [
12 | {
13 | id: 'name',
14 | key: 'DeviceName',
15 | label: 'Phone',
16 | sort: 'string',
17 | filter: 'like',
18 | template: ' ' +
19 | ' ' +
20 | '{{ row.DeviceName }}'
21 | },
22 | {
23 | id: 'brand',
24 | key: 'Brand',
25 | sort: 'string',
26 | label: 'Brand'
27 | },
28 | {
29 | id: 'tech',
30 | key: 'technology',
31 | sort: 'string',
32 | label: 'Tech'
33 | }
34 | ];
35 | $scope.phoneData = phoneData;
36 |
37 | });
38 |
--------------------------------------------------------------------------------
/app/scripts/controllers/disabled-columns.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 | .controller('DisabledColumnsCtrl', function($scope, $q, phoneData, $templateCache) {
5 |
6 | this.my_table_options = {
7 | bodyHeight: 600,
8 | rowPadding: 600,
9 | storage: localStorage,
10 | storageKey: 'example'
11 | };
12 | this.my_table_options_paginated = angular.extend({ pagingStrategy: 'PAGINATE' }, this.my_table_options);
13 | this.my_selected_rows = [];
14 | this.my_table_columns = [
15 | {
16 | id: 'name',
17 | key: 'DeviceName',
18 | label: 'Phone',
19 | sort: 'string',
20 | filter: 'like',
21 | template: '' +
22 | ' ' +
23 | ' ' +
24 | '{{ row.DeviceName }}' +
25 | ' '
26 | },
27 | {
28 | id: 'brand',
29 | key: 'Brand',
30 | sort: 'string',
31 | label: 'Brand'
32 | },
33 | {
34 | id: 'edge',
35 | key: 'edge',
36 | label: 'Edge',
37 | sort: 'string',
38 | filter: 'like'
39 | },
40 | {
41 | id: 'tech',
42 | key: 'technology',
43 | sort: 'string',
44 | label: 'Tech'
45 | }
46 | ];
47 | this.my_enabled_columns = this.my_table_columns.map(function(c) { return c.id; });
48 | this.phoneData = phoneData;
49 | var _this = this;
50 |
51 | this.toggleColumn = function(id) {
52 | var curIndexOfColumnId = _this.my_enabled_columns.indexOf(id);
53 | if (curIndexOfColumnId > -1) {
54 | _this.my_enabled_columns.splice(curIndexOfColumnId, 1);
55 | } else {
56 | _this.my_enabled_columns.push(id);
57 | var enabled = _this.my_enabled_columns;
58 | // Do this to preserve original order for this demo. Do whatever you want in your own use-case
59 | _this.my_enabled_columns = _this.my_table_columns
60 | .filter(function(c) { return enabled.indexOf(c.id) > -1; })
61 | .map(function(c) { return c.id; });
62 | }
63 | };
64 |
65 | });
66 |
--------------------------------------------------------------------------------
/app/scripts/controllers/expandable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 | .controller('ExpandableCtrl', function($scope, $q, phoneData, $templateCache) {
5 |
6 | $scope.my_table_options = {
7 | expandableTemplateUrl: 'views/expandable-panel.html',
8 | bodyHeight: 600,
9 | rowPadding: 600
10 | };
11 | $scope.my_table_options_paginated = angular.extend({ pagingStrategy: 'PAGINATE' }, $scope.my_table_options);
12 | $scope.my_selected_rows = [];
13 | $scope.my_table_columns = [
14 | {
15 | id: 'name',
16 | key: 'DeviceName',
17 | label: 'Phone',
18 | sort: 'string',
19 | filter: 'like',
20 | template: '' +
21 | ' ' +
22 | ' ' +
23 | '{{ row.DeviceName }}' +
24 | ' '
25 | },
26 | {
27 | id: 'brand',
28 | key: 'Brand',
29 | sort: 'string',
30 | label: 'Brand'
31 | },
32 | {
33 | id: 'tech',
34 | key: 'technology',
35 | sort: 'string',
36 | label: 'Tech'
37 | }
38 | ];
39 | $scope.phoneData = phoneData;
40 |
41 | });
42 |
--------------------------------------------------------------------------------
/app/scripts/controllers/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 |
5 | // angular filter, to be used with the "ngFilter" option in column definitions below
6 | .filter('commaGroups', function() {
7 |
8 | // Converts a number like 123456789 to string with appropriate commas: "123,456,789"
9 | function commaGroups(value) {
10 | if (typeof value === 'undefined') {
11 | return '-';
12 | }
13 | var parts = value.toString().split('.');
14 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
15 | return parts.join('.');
16 | }
17 | return commaGroups;
18 | })
19 | .controller('MainCtrl', function ($scope, $templateCache, $q) {
20 |
21 | // Format functions, used with the "format" option in column definitions below
22 | // converts number in inches to display string, eg. 69 => 5'9"
23 | function inches2feet(inches, model){
24 | var feet = Math.floor(inches/12);
25 | inches = inches % 12;
26 | return feet + '\'' + inches + '"';
27 | }
28 | // Custom column filtering function:
29 | // If the user types "tall", only people who
30 | // are taller than 70 inches will be displayed.
31 | function feet_filter(term, value, formatted, model) {
32 | if (term === 'tall') { return value > 70; }
33 | if (term === 'short') { return value < 69; }
34 | return true;
35 | }
36 | feet_filter.title = 'Type in "short" or "tall"';
37 |
38 | // Generates `num` random rows
39 | function genRows(num){
40 | var retVal = [];
41 | for (var i=0; i < num; i++) {
42 | retVal.push(genRow(i));
43 | }
44 | return retVal;
45 | }
46 |
47 | // Generates a row with random data
48 | function genRow(id){
49 |
50 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz'];
51 | var lnames = ['sterling','smith','erickson','burke','ansari'];
52 | var seed = Math.random();
53 | var seed2 = Math.random();
54 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ];
55 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ];
56 |
57 | return {
58 | id: id,
59 | selected: false,
60 | first_name: first_name,
61 | last_name: last_name,
62 | age: Math.ceil(seed * 75) + 15,
63 | height: Math.round( seed2 * 36 ) + 48,
64 | weight: Math.round( seed2 * 130 ) + 90,
65 | likes: Math.round(seed2 * seed * 1000000)
66 | };
67 | }
68 |
69 | // Simulate location of template file
70 | $templateCache.put('path/to/example/template.html', '{{row[column.key]}} ');
71 | $templateCache.put('path/to/example/labelTemplate.html', 'Weight ');
72 |
73 | // Table column definition objects
74 | $scope.my_table_columns = [
75 | { id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true },
76 | //{ id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true, selectObject: true },
77 | { id: 'ID', key: 'id', label: 'id', sort: 'number', filter: 'number' },
78 | { id: 'first_name', key: 'first_name', label: 'First Name', sort: 'string', filter: 'like', template: '{{row[column.key]}} ' },
79 | { id: 'last_name', key: 'last_name', label: 'Last Name', sort: 'string', filter: 'like', templateUrl: 'path/to/example/template.html' },
80 | { id: 'age', key: 'age', label: 'Age', sort: 'number', filter: 'number' },
81 | { id: 'likes', key: 'likes', labelTemplate: ' ', ngFilter: 'commaGroups' },
82 | { id: 'height', key: 'height', label: 'Height', format: inches2feet, filter: feet_filter, sort: 'number' },
83 | { id: 'weight', key: 'weight', labelTemplateUrl: 'path/to/example/labelTemplate.html', filter: 'number', sort: 'number' }
84 | ];
85 |
86 | // Table data
87 | $scope.my_table_data = [];
88 |
89 |
90 | // Selected rows
91 | $scope.my_selected_rows = [];
92 |
93 | // table options
94 | var dataDfd = $q.defer();
95 | $scope.my_table_options = {
96 | rowLimit: 10,
97 | storage: localStorage,
98 | storageKey: 'gh-page-table',
99 | storageHash: 'a9s8df9a8s7df98as7df',
100 | // getter: function(key, row) {
101 | // return row[key];
102 | // },
103 | loading: true,
104 | loadingPromise: dataDfd.promise
105 | };
106 | $scope.my_table_options_paginate = angular.extend({}, $scope.my_table_options, {
107 | pagingStrategy: 'PAGINATE',
108 | rowsPerPage: 8
109 | });
110 |
111 | // kick off interval that updates the dataset
112 | setInterval(function() {
113 | $scope.my_table_data = genRows(1000);
114 | dataDfd.resolve();
115 | $scope.$apply();
116 | }, 1000);
117 |
118 | });
119 |
--------------------------------------------------------------------------------
/app/scripts/controllers/max-height.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 |
5 | // angular filter, to be used with the "ngFilter" option in column definitions below
6 | .filter('commaGroups', function() {
7 |
8 | // Converts a number like 123456789 to string with appropriate commas: "123,456,789"
9 | function commaGroups(value) {
10 | if (typeof value === 'undefined') {
11 | return '-';
12 | }
13 | var parts = value.toString().split('.');
14 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
15 | return parts.join('.');
16 | }
17 | return commaGroups;
18 | })
19 | .controller('MaxHeightCtrl', function ($scope, $templateCache, $q) {
20 |
21 | // Format functions, used with the "format" option in column definitions below
22 | // converts number in inches to display string, eg. 69 => 5'9"
23 | function inches2feet(inches, model){
24 | var feet = Math.floor(inches/12);
25 | inches = inches % 12;
26 | return feet + '\'' + inches + '"';
27 | }
28 | // Custom column filtering function:
29 | // If the user types "tall", only people who
30 | // are taller than 70 inches will be displayed.
31 | function feet_filter(term, value, formatted, model) {
32 | if (term === 'tall') { return value > 70; }
33 | if (term === 'short') { return value < 69; }
34 | return true;
35 | }
36 | feet_filter.title = 'Type in "short" or "tall"';
37 |
38 | // Generates `num` random rows
39 | function genRows(num){
40 | var retVal = [];
41 | for (var i=0; i < num; i++) {
42 | retVal.push(genRow(i));
43 | }
44 | return retVal;
45 | }
46 |
47 | // Generates a row with random data
48 | function genRow(id){
49 |
50 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz'];
51 | var lnames = ['sterling','smith','erickson','burke','ansari'];
52 | var seed = Math.random();
53 | var seed2 = Math.random();
54 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ];
55 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ];
56 |
57 | return {
58 | id: id,
59 | selected: false,
60 | first_name: first_name,
61 | last_name: last_name,
62 | age: Math.ceil(seed * 75) + 15,
63 | height: Math.round( seed2 * 36 ) + 48,
64 | weight: Math.round( seed2 * 130 ) + 90,
65 | likes: Math.round(seed2 * seed * 1000000)
66 | };
67 | }
68 |
69 | // Simulate location of template file
70 | $templateCache.put('path/to/example/template.html', '{{row[column.key]}} ');
71 |
72 | // Table column definition objects
73 | $scope.my_table_columns = [
74 | { id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true },
75 | { id: 'ID', key: 'id', label: 'ID', sort: 'number', filter: 'number' },
76 | { id: 'first_name', key: 'first_name', label: 'First Name', sort: 'string', filter: 'like', template: '{{row[column.key]}} ' },
77 | { id: 'last_name', key: 'last_name', label: 'Last Name', sort: 'string', filter: 'like', templateUrl: 'path/to/example/template.html' },
78 | { id: 'age', key: 'age', label: 'Age', sort: 'number', filter: 'number' },
79 | { id: 'likes', key: 'likes', label: 'likes', ngFilter: 'commaGroups' },
80 | { id: 'height', key: 'height', label: 'Height', format: inches2feet, filter: feet_filter, sort: 'number' },
81 | { id: 'weight', key: 'weight', label: 'Weight', filter: 'number', sort: 'number' }
82 | ];
83 |
84 | // Table data
85 | $scope.my_table_data = [];
86 |
87 |
88 | // Selected rows
89 | $scope.my_selected_rows = [];
90 |
91 | // table options
92 | var apiDfd = $q.defer();
93 | $scope.my_table_options = {
94 | rowLimit: 10,
95 | storage: localStorage,
96 | storageKey: 'gh-page-table',
97 | loading: true,
98 | onRegisterApi: function(api) {
99 | $scope.api = api;
100 | apiDfd.resolve();
101 | }
102 | };
103 |
104 | // kick off interval that updates the dataset
105 | var id = 1;
106 | setInterval(function() {
107 | if (id < 1000) {
108 | $scope.my_table_data.push(genRow(id++));
109 | $scope.$apply();
110 | apiDfd.promise.then(function() {
111 | $scope.api.setLoading(false);
112 | });
113 | }
114 | }, 1000);
115 |
116 | });
117 |
--------------------------------------------------------------------------------
/app/scripts/controllers/perf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 | .controller('PerfCtrl', function ($scope) {
5 |
6 | // Format functions
7 | function inches2feet(inches, model){
8 | var feet = Math.floor(inches/12);
9 | inches = inches % 12;
10 | return feet + '\'' + inches + '"';
11 | }
12 | function feet_filter(term, value, formatted, model) {
13 | if (term === 'tall') { return value > 70; }
14 | if (term === 'short') { return value < 69; }
15 | return true;
16 | }
17 | feet_filter.title = 'Type in "short" or "tall"';
18 |
19 | // Random data generator
20 | function genRows(num){
21 | var retVal = [];
22 | for (var i=0; i < num; i++) {
23 | retVal.push(genRow(i));
24 | }
25 | return retVal;
26 | }
27 | function genRow(id){
28 |
29 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz'];
30 | var lnames = ['sterling','smith','erickson','burke','ansari'];
31 | var seed = Math.random();
32 | var seed2 = Math.random();
33 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ];
34 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ];
35 |
36 | return {
37 | id: id,
38 | selected: false,
39 | first_name: first_name,
40 | last_name: last_name,
41 | age: Math.ceil(seed * 75) + 15,
42 | height: Math.round( seed2 * 36 ) + 48,
43 | weight: Math.round( seed2 * 130 ) + 90
44 | };
45 | }
46 |
47 | // Table columns
48 | $scope.my_table_columns = [
49 | { id: 'selected', key: 'id', label: '', width: 30, lockWidth: true, selector: true },
50 | { id: 'ID', key: 'id', label: 'ID', sort: 'number', filter: 'number' },
51 | { id: 'first_name', key: 'first_name', label: 'First Name', sort: 'string', filter: 'like' },
52 | { id: 'last_name', key: 'last_name', label: 'Last Name', sort: 'string', filter: 'like' },
53 | { id: 'age', key: 'age', label: 'Age', sort: 'number', filter: 'number' },
54 | { id: 'height', key: 'height', label: 'Height', format: inches2feet, filter: feet_filter, sort: 'number' },
55 | { id: 'weight', key: 'weight', label: 'Weight', filter: 'number', sort: 'number' }
56 | ];
57 |
58 | // Table data
59 | $scope.my_table_data = genRows(30);
60 | $scope.my_table_data2 = genRows(40);
61 | $scope.my_table_data3 = genRows(50);
62 | $scope.my_table_data4 = genRows(60);
63 | $scope.my_table_data5 = genRows(70);
64 | $scope.my_table_data6 = genRows(80);
65 | $scope.my_table_data7 = genRows(90);
66 |
67 |
68 | // Selected rows
69 | $scope.my_selected_rows = [];
70 | $scope.my_selected_rows2 = [];
71 | $scope.my_selected_rows3 = [];
72 | $scope.my_selected_rows4 = [];
73 | $scope.my_selected_rows5 = [];
74 | $scope.my_selected_rows6 = [];
75 | $scope.my_selected_rows7 = [];
76 |
77 | // table options
78 | $scope.my_table_options = {
79 | rowLimit: 10,
80 | storage: localStorage,
81 | storageKey: 'gh-page-table'
82 | };
83 |
84 | $scope.my_table_options2 = {
85 | rowLimit: 10,
86 | storage: localStorage,
87 | storageKey: 'gh-page-table2'
88 | };
89 |
90 | $scope.my_table_options3 = {
91 | rowLimit: 10,
92 | storage: localStorage,
93 | storageKey: 'gh-page-table3'
94 | };
95 |
96 | $scope.my_table_options4 = {
97 | rowLimit: 10,
98 | storage: localStorage,
99 | storageKey: 'gh-page-table4'
100 | };
101 |
102 | $scope.my_table_options5 = {
103 | rowLimit: 10,
104 | storage: localStorage,
105 | storageKey: 'gh-page-table5'
106 | };
107 |
108 | $scope.my_table_options6 = {
109 | rowLimit: 10,
110 | storage: localStorage,
111 | storageKey: 'gh-page-table6'
112 | };
113 |
114 | $scope.my_table_options7 = {
115 | rowLimit: 10,
116 | storage: localStorage,
117 | storageKey: 'gh-page-table7'
118 | };
119 |
120 | setInterval(function() {
121 | $scope.my_table_data = genRows(30);
122 | $scope.my_table_data2 = genRows(40);
123 | $scope.my_table_data3 = genRows(50);
124 | $scope.my_table_data4 = genRows(60);
125 | $scope.my_table_data5 = genRows(70);
126 | $scope.my_table_data6 = genRows(80);
127 | $scope.my_table_data7 = genRows(90);
128 | // $scope.my_table_data = genRows(30);
129 | $scope.$apply();
130 | }, 1000);
131 |
132 | });
133 |
--------------------------------------------------------------------------------
/app/scripts/controllers/server-side.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.ghPage')
4 |
5 | .controller('ServerSideCtrl', function ($scope, $templateCache, $q, phoneData, $timeout) {
6 |
7 | // Mock serverside call
8 | function getData(offset, limit, activeFilters, activeSorts) {
9 |
10 | var dfd = $q.defer();
11 |
12 |
13 | // Simulate back-end errors:
14 |
15 | // if (Math.random() < 0.2) {
16 | // $timeout(function() {
17 | // dfd.reject({ message: 'an error occurred' });
18 | // }, 200);
19 | // return dfd.promise;
20 | // }
21 |
22 | $timeout(function() {
23 |
24 | var results = phoneData;
25 |
26 | // Perform filters first:
27 | if (activeFilters.length) {
28 | results = results.filter(function(row) {
29 | for (var i = 0; i < activeFilters.length; i++) {
30 | var filterMeta = activeFilters[i];
31 | var columnDef = filterMeta.column;
32 | var filterString = filterMeta.value.toLowerCase();
33 |
34 | if (columnDef.filter === 'string') {
35 | var searchableValue = row[columnDef.key].toLowerCase();
36 | // console.log(searchableValue, filterString);
37 | if (searchableValue.indexOf(filterString) === -1) {
38 | return false;
39 | } else {
40 | return true;
41 | }
42 | }
43 | }
44 | });
45 | }
46 |
47 | // Perform sorts
48 | if (activeSorts.length) {
49 | results = results.sort(function(rowA, rowB) {
50 | for (var i = 0; i < activeSorts.length; i++) {
51 | var sortMeta = activeSorts[i];
52 | var columnDef = sortMeta.column;
53 | var sortDirection = sortMeta.direction;
54 |
55 | if (columnDef.sort === 'string' && angular.isString(rowA[columnDef.key]) && angular.isString(rowB[columnDef.key])) {
56 | var comparisonResult = sortDirection === 'ASC' ? rowA[columnDef.key].localeCompare(rowB[columnDef.key]) : rowB[columnDef.key].localeCompare(rowA[columnDef.key]);
57 | if (comparisonResult !== 0) {
58 | return comparisonResult;
59 | }
60 | }
61 | }
62 | return 0;
63 | });
64 | }
65 |
66 | dfd.resolve({
67 | total: results.length,
68 | rows: results.slice(offset, offset + limit)
69 | });
70 |
71 | }, Math.random() * 1000);
72 |
73 | return dfd.promise;
74 | }
75 |
76 | $scope.my_table_options = {
77 | getData: getData
78 | };
79 |
80 | $scope.my_table_options_paginate = angular.extend({}, $scope.my_table_options, {
81 | pagingStrategy: 'PAGINATE'
82 | });
83 |
84 | $scope.my_table_columns = [
85 | {
86 | id: 'DeviceName',
87 | label: 'Device Name',
88 | key: 'DeviceName',
89 | // The filter and sort values are only checked for truthiness by angular-mesa when getData is used
90 | filter: 'string',
91 | filterPlaceholder: 'filter name',
92 | sort: 'string'
93 | },
94 | {
95 | id: 'Brand',
96 | label: 'Brand',
97 | key: 'Brand',
98 | filter: 'string',
99 | filterPlaceholder: 'filter brand',
100 | sort: 'string'
101 | },
102 | {
103 | id: 'weight',
104 | label: 'weight',
105 | key: 'weight',
106 | sort: 'string'
107 | },
108 | {
109 | id: 'size',
110 | label: 'size',
111 | key: 'size'
112 | }
113 | ];
114 |
115 | });
116 |
--------------------------------------------------------------------------------
/app/views/clickable-rows.html:
--------------------------------------------------------------------------------
1 | Clickable Rows
2 |
3 | This example shows the usage of the clickable rows feature, where you can define a custom click handler for each row.
4 |
5 |
6 |
16 |
17 |
24 |
25 |
--------------------------------------------------------------------------------
/app/views/disabled-columns.html:
--------------------------------------------------------------------------------
1 | Disabled Columns
2 |
3 | This example shows how you can enable/disable columns dynamically.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | scroll
17 | paginate
23 |
24 |
25 |
35 |
36 |
46 |
47 |
48 |
49 |
50 |
Enabled Columns
51 |
52 |
53 |
54 | {{ column.label }}
55 |
56 |
57 |
{{ vm.my_enabled_columns | json }}
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/views/expandable-panel.html:
--------------------------------------------------------------------------------
1 | Specs for {{ row.DeviceName }}:
2 |
3 |
4 | gprs : {{ row.gprs }}
5 | edge : {{ row.edge }}
6 | announced : {{ row.announced }}
7 | status : {{ row.status }}
8 | dimensions : {{ row.dimensions }}
9 | weight : {{ row.weight }}
10 | sim : {{ row.sim }}
11 | type : {{ row.type }}
12 | size : {{ row.size }}
13 | resolution : {{ row.resolution }}
14 | card_slot : {{ row.card_slot }}
15 | alert_types : {{ row.alert_types }}
16 | loudspeaker_ : {{ row.loudspeaker_ }}
17 |
18 |
19 | wlan : {{ row.wlan }}
20 | bluetooth : {{ row.bluetooth }}
21 | gps : {{ row.gps }}
22 | radio : {{ row.radio }}
23 | usb : {{ row.usb }}
24 | messaging : {{ row.messaging }}
25 | browser : {{ row.browser }}
26 | java : {{ row.java }}
27 | features_c : {{ row.features_c }}/video editor',
28 | battery_c : {{ row.battery_c }}
29 | stand_by : {{ row.stand_by }}
30 | talk_time : {{ row.talk_time }}
31 | colors : {{ row.colors }}
32 |
33 |
34 | sensors : {{ row.sensors }}
35 | cpu : {{ row.cpu }}
36 | internal : {{ row.internal }}
37 | os : {{ row.os }}
38 | primary_ : {{ row.primary_ }}
39 | video : {{ row.video }}
40 | secondary : {{ row.secondary }}
41 | speed : {{ row.speed }}
42 | network_c : {{ row.network_c }}
43 | chipset : {{ row.chipset }}
44 | features : {{ row.features }}
45 | gpu : {{ row.gpu }}
46 | multitouch : {{ row.multitouch }}
47 | _2g_bands : {{ row._2g_bands }}
48 | _3_5mm_jack_ : {{ row._3_5mm_jack_ }}
49 | _3g_bands : {{ row._3g_bands }}
50 |
51 |
--------------------------------------------------------------------------------
/app/views/expandable.html:
--------------------------------------------------------------------------------
1 | Expandable Rows
2 |
3 | This example shows the usage of the expandable rows feature, where rows can be expanded with arbitrary content.
4 |
5 |
6 |
7 |
8 | scroll
14 | paginate
20 |
21 |
30 |
31 |
40 |
41 |
--------------------------------------------------------------------------------
/app/views/main.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 | Kitchen Sink Example:
28 |
29 | This example showcases most of the features available for this module. Specifically, it shows row selection, column sorting, built-in column filters, custom column filters, column resizing, locked column width, cell formatting, template string for cell markup, templateUrl for cell markup, and use of localStorage for persistence between page loads.
30 |
31 |
32 |
33 |
34 |
35 | scroll
41 | paginate
47 |
48 |
49 |
56 |
57 |
58 |
Selected rows:
59 |
{{my_selected_rows | json}}
60 |
61 |
62 |
63 |
64 |
71 |
72 |
73 |
Selected rows:
74 |
{{my_selected_rows | json}}
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/app/views/max-height.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | This content should be pushed down as more rows come into the table.
10 |
--------------------------------------------------------------------------------
/app/views/perf.html:
--------------------------------------------------------------------------------
1 | Performance Test:
2 |
3 | This page tests the performance of 7 independent instances of tables on the page, each with a significant number of rows.
4 |
5 |
6 |
7 |
14 |
15 |
16 |
23 |
24 |
25 |
32 |
33 |
34 |
41 |
42 |
43 |
50 |
51 |
52 |
59 |
60 |
61 |
68 |
69 |
--------------------------------------------------------------------------------
/app/views/server-side-interaction.html:
--------------------------------------------------------------------------------
1 | Server-side Interaction
2 |
3 | This example shows the usage of the server-side interaction feature, where rows can be fetched from a back-end.
4 |
5 |
6 |
7 | scroll
13 | paginate
19 |
20 |
21 |
26 |
27 |
28 |
33 |
34 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angularjs-table",
3 | "version": "2.21.5",
4 | "main": [
5 | "./dist/ap-mesa.js",
6 | "./dist/ap-mesa.css"
7 | ],
8 | "dependencies": {
9 | "angular": "^1.6",
10 | "jquery": "~2.0.3",
11 | "angular-sanitize": "^1.6",
12 | "angular-ui-sortable": "~0.13",
13 | "jquery-ui": ">=1.11.0"
14 | },
15 | "devDependencies": {
16 | "angular-route": "^1.6",
17 | "bootstrap": "~3.0.3",
18 | "angular-mocks": "^1.6",
19 | "angular-animate": "^1.6",
20 | "mocha": "~1.18.2",
21 | "chai": "~1.9.1",
22 | "sinon": "~1.9.0",
23 | "sinon-chai": "~2.5.0"
24 | },
25 | "ignore": [
26 | "src",
27 | "vendor",
28 | "app",
29 | "node_modules",
30 | "test",
31 | "**/.*"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/dist/ap-mesa.css:
--------------------------------------------------------------------------------
1 | /* main root element */
2 |
3 | .ap-mesa-wrapper {
4 | position: relative;
5 | }
6 |
7 | /* styles for both header- and rows- tables */
8 |
9 | .ap-mesa {
10 | table-layout: fixed;
11 | width: 100%;
12 | margin-bottom: 0;
13 | }
14 |
15 | /* the visible table header */
16 |
17 | .mesa-header-table {
18 | border-bottom: none;
19 | }
20 |
21 | .mesa-header-table thead > tr > th {
22 | border-width: 1px;
23 | }
24 |
25 | /* the invisible table header; used for correct column widths */
26 |
27 | .mesa-rows-table thead {
28 | height: 0;
29 | visibility: hidden;
30 | }
31 |
32 | .mesa-rows-table > thead > tr > th {
33 | border-width: 0;
34 | padding: 0 !important;
35 | }
36 |
37 | .mesa-rows-table-wrapper {
38 | overflow: auto;
39 | }
40 |
41 | .mesa-rows-table > tbody + tbody {
42 | border-top: none;
43 | }
44 |
45 | /* general styles for all cells in both tables */
46 |
47 | .ap-mesa th {
48 | white-space: nowrap;
49 | position: relative;
50 | }
51 |
52 | .ap-mesa td {
53 | word-wrap: break-word;
54 | overflow: hidden;
55 | }
56 |
57 | /* this type of cell is used to show messages like "loading" */
58 |
59 | .ap-mesa td.space-holder-row-cell {
60 | text-align: center;
61 | }
62 |
63 | /* search input */
64 |
65 | .ap-mesa tr.ap-mesa-filter-row td input {
66 | width: 100%;
67 | border-radius: 2em;
68 | border: 1px solid #CCC;
69 | outline: none;
70 | text-indent: 0.3em;
71 | font-size: 90%;
72 | }
73 |
74 | .ap-mesa tr.ap-mesa-filter-row td {
75 | position: relative;
76 | }
77 |
78 | /* button to clear search */
79 |
80 | .ap-mesa tr.ap-mesa-filter-row td .clear-search-btn {
81 | position: absolute;
82 | border-radius: 50%;
83 | border: none;
84 | right: 1em;
85 | top: 50%;
86 | -webkit-transform: translateY(-50%);
87 | -ms-transform: translateY(-50%);
88 | transform: translateY(-50%);
89 | font-size: 12px;
90 | opacity: 0.2;
91 | color: white;
92 | background-color: black;
93 | padding: 0;
94 | width: 15px;
95 | line-height: 15px;
96 | }
97 |
98 | /* activated search input, as in when there is text in it */
99 |
100 | .ap-mesa tr.ap-mesa-filter-row td input.active {
101 | background-color: #3D82C2;
102 | color: #FFF;
103 | border-color: #747474;
104 | }
105 |
106 | /* placeholder object for when columns are being sorted */
107 |
108 | .ap-mesa .ap-mesa-column-placeholder {
109 | background-color: #DDD;
110 | }
111 |
112 | /* handle to grab in order to resize a column */
113 |
114 | .ap-mesa th .column-resizer {
115 | position: absolute;
116 | top: 0;
117 | right: 0;
118 | width: 5px;
119 | height: 100%;
120 | border-width: 0 1px;
121 | cursor: col-resize;
122 | border-color: #DDD;
123 | border-style: solid;
124 | }
125 |
126 | /* this class is applied to a .column-resizer
127 | when a discreet width has been set on it */
128 |
129 | .ap-mesa th .column-resizer.discreet-width {
130 | background-color: #DDD;
131 | }
132 |
133 | /* wrapper for text in a th */
134 |
135 | .ap-mesa th .column-text {
136 | max-width: 100%;
137 | overflow: hidden;
138 | display: block;
139 | }
140 |
141 | /* the element showing what the new width will be */
142 |
143 | .ap-mesa th .column-resizer-marquee {
144 | left: 0;
145 | top: 0;
146 | height: 100%;
147 | border: 1px dotted #DEDEDE;
148 | position: absolute;
149 | }
150 |
151 | /* sorting icons */
152 |
153 | .ap-mesa th span.sorting-icon {
154 | font-size: 10px;
155 | }
156 |
157 | .ap-mesa th span.sort-priority {
158 | background-color: black;
159 | color: white;
160 | font-size: 9px;
161 | font-weight: bold;
162 | vertical-align: top;
163 | display: inline-block;
164 | height: 12px;
165 | width: 12px;
166 | text-align: center;
167 | border-radius: 50%;
168 | }
169 |
170 | .ap-mesa th span.glyphicon-sort {
171 | opacity: 0.2;
172 | }
173 |
174 | .ap-mesa th.sortable-column {
175 | cursor: pointer;
176 | -webkit-user-select: none;
177 | -moz-user-select: none;
178 | -ms-user-select: none;
179 | user-select: none;
180 | }
181 |
182 | /* dummy row */
183 |
184 | .ap-mesa-dummy-row {
185 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAlCAYAAACDKIOpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABtJREFUeNpiuHv37n+G////MzAxAMHQIQACDAC7twbaN2nkgwAAAABJRU5ErkJggg==');
186 | background-repeat: repeat;
187 | }
188 |
189 | table tbody .ap-mesa-dummy-row td {
190 | border-top: none;
191 | padding: 0;
192 | }
193 |
194 | .ap-mesa-pagination {
195 | -webkit-user-select: none;
196 | -moz-user-select: none;
197 | -ms-user-select: none;
198 | user-select: none;
199 | overflow: hidden;
200 | }
201 |
202 | .ap-mesa-pagination .rows-per-page-ctrl {
203 | float: right;
204 | }
205 |
206 | .ap-mesa-pagination .rows-per-page-ctrl .pagination {
207 | vertical-align: middle;
208 | }
209 |
210 | .ap-mesa-pagination .rows-per-page-msg {
211 | vertical-align: middle;
212 | }
213 |
214 | .ap-mesa-pagination ul.pagination li a {
215 | cursor: pointer;
216 | }
217 |
218 | /* async loader styles */
219 |
220 | .ap-mesa-status-display-wrapper {
221 | position: relative;
222 | text-align: center;
223 | }
224 |
225 | .ap-mesa-status-display {
226 | background: rgba(0,0,0,0.3);
227 | border-radius: 1rem;
228 | width: 20%;
229 | max-width: 130px;
230 | min-width: 100px;
231 | height: auto;
232 | padding: 0.5rem 1rem;
233 | position: relative;
234 | display: inline-block;
235 | margin-top: 0.7rem;
236 | }
237 |
238 | .ap-mesa-loading-display.ng-enter {
239 | -webkit-transition: opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
240 | transition: opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
241 | }
242 |
243 | .paging-strategy-scroll .ap-mesa-loading-display.ng-enter {
244 | -webkit-transition-delay: 0.4s;
245 | transition-delay: 0.4s;
246 | }
247 |
248 | .paging-strategy-paginate .ap-mesa-loading-display.ng-enter {
249 | -webkit-transition-delay: 0s;
250 | transition-delay: 0s;
251 | }
252 |
253 | .ap-mesa-loading-display.ng-enter,
254 | .ap-mesa-loading-display.ng-enter.ng-leave.ng-leave-active {
255 | opacity: 0;
256 | }
257 |
258 | .ap-mesa-loading-display.ng-enter.ng-leave,
259 | .ap-mesa-loading-display.ng-enter.ng-enter-active {
260 | opacity: 1;
261 | }
262 |
263 | .ap-mesa-status-display-wrapper.has-rows .ap-mesa-status-display {
264 | position: absolute;
265 | -webkit-transform: translateX(-50%);
266 | -ms-transform: translateX(-50%);
267 | transform: translateX(-50%);
268 | }
269 |
270 | .ap-mesa-error-display-inner {
271 | color: red;
272 | display: inline-block;
273 | }
274 |
275 | /* credit to: https://projects.lukehaas.me/css-loaders/ */
276 |
277 | .ap-mesa-status-display-inner,
278 | .ap-mesa-status-display-inner:before,
279 | .ap-mesa-status-display-inner:after {
280 | border-radius: 50%;
281 | width: 1.5em;
282 | height: 1.5em;
283 | -webkit-animation-fill-mode: both;
284 | animation-fill-mode: both;
285 | -webkit-animation: load7 1.8s infinite ease-in-out;
286 | animation: load7 1.8s infinite ease-in-out;
287 | }
288 |
289 | .ap-mesa-status-display-inner {
290 | color: #ffffff;
291 | font-size: 10px;
292 | margin: 0 auto;
293 | position: relative;
294 | text-indent: -9999em;
295 | -webkit-transform: translateZ(0) translateY(-100%);
296 | -ms-transform: translateZ(0) translateY(-100%);
297 | transform: translateZ(0) translateY(-100%);
298 | -webkit-animation-delay: -0.16s;
299 | animation-delay: -0.16s;
300 | }
301 |
302 | .ap-mesa-status-display-inner:before,
303 | .ap-mesa-status-display-inner:after {
304 | content: '';
305 | position: absolute;
306 | top: 0;
307 | }
308 |
309 | .ap-mesa-status-display-inner:before {
310 | left: -3.5em;
311 | -webkit-animation-delay: -0.32s;
312 | animation-delay: -0.32s;
313 | }
314 |
315 | .ap-mesa-status-display-inner:after {
316 | left: 3.5em;
317 | }
318 |
319 | @-webkit-keyframes load7 {
320 | 0%, 80%, 100% {
321 | -webkit-box-shadow: 0 1.5em 0 -1.3em;
322 | box-shadow: 0 1.5em 0 -1.3em;
323 | }
324 |
325 | 40% {
326 | -webkit-box-shadow: 0 1.5em 0 0;
327 | box-shadow: 0 1.5em 0 0;
328 | }
329 | }
330 |
331 | @keyframes load7 {
332 | 0%, 80%, 100% {
333 | -webkit-box-shadow: 0 1.5em 0 -1.3em;
334 | box-shadow: 0 1.5em 0 -1.3em;
335 | }
336 |
337 | 40% {
338 | -webkit-box-shadow: 0 1.5em 0 0;
339 | box-shadow: 0 1.5em 0 0;
340 | }
341 | }
--------------------------------------------------------------------------------
/dist/ap-mesa.min.css:
--------------------------------------------------------------------------------
1 | .ap-mesa-wrapper{position:relative}.ap-mesa{table-layout:fixed;width:100%;margin-bottom:0}.mesa-header-table{border-bottom:0}.mesa-header-table thead>tr>th{border-width:1px}.mesa-rows-table thead{height:0;visibility:hidden}.mesa-rows-table>thead>tr>th{border-width:0;padding:0!important}.mesa-rows-table-wrapper{overflow:auto}.mesa-rows-table>tbody+tbody{border-top:0}.ap-mesa th{white-space:nowrap;position:relative}.ap-mesa td{word-wrap:break-word;overflow:hidden}.ap-mesa td.space-holder-row-cell{text-align:center}.ap-mesa tr.ap-mesa-filter-row td input{width:100%;border-radius:2em;border:1px solid #CCC;outline:0;text-indent:.3em;font-size:90%}.ap-mesa tr.ap-mesa-filter-row td{position:relative}.ap-mesa tr.ap-mesa-filter-row td .clear-search-btn{position:absolute;border-radius:50%;border:0;right:1em;top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);font-size:12px;opacity:.2;color:#fff;background-color:#000;padding:0;width:15px;line-height:15px}.ap-mesa tr.ap-mesa-filter-row td input.active{background-color:#3D82C2;color:#FFF;border-color:#747474}.ap-mesa .ap-mesa-column-placeholder{background-color:#DDD}.ap-mesa th .column-resizer{position:absolute;top:0;right:0;width:5px;height:100%;border-width:0 1px;cursor:col-resize;border-color:#DDD;border-style:solid}.ap-mesa th .column-resizer.discreet-width{background-color:#DDD}.ap-mesa th .column-text{max-width:100%;overflow:hidden;display:block}.ap-mesa th .column-resizer-marquee{left:0;top:0;height:100%;border:1px dotted #DEDEDE;position:absolute}.ap-mesa th span.sorting-icon{font-size:10px}.ap-mesa th span.sort-priority{background-color:#000;color:#fff;font-size:9px;font-weight:700;vertical-align:top;display:inline-block;height:12px;width:12px;text-align:center;border-radius:50%}.ap-mesa th span.glyphicon-sort{opacity:.2}.ap-mesa th.sortable-column{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ap-mesa-dummy-row{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAlCAYAAACDKIOpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABtJREFUeNpiuHv37n+G////MzAxAMHQIQACDAC7twbaN2nkgwAAAABJRU5ErkJggg==);background-repeat:repeat}table tbody .ap-mesa-dummy-row td{border-top:0;padding:0}.ap-mesa-pagination{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden}.ap-mesa-pagination .rows-per-page-ctrl{float:right}.ap-mesa-pagination .rows-per-page-ctrl .pagination,.ap-mesa-pagination .rows-per-page-msg{vertical-align:middle}.ap-mesa-pagination ul.pagination li a{cursor:pointer}.ap-mesa-status-display-wrapper{position:relative;text-align:center}.ap-mesa-status-display{background:rgba(0,0,0,.3);border-radius:1rem;width:20%;max-width:130px;min-width:100px;height:auto;padding:.5rem 1rem;position:relative;display:inline-block;margin-top:.7rem}.ap-mesa-loading-display.ng-enter{-webkit-transition:opacity cubic-bezier(0.25,.46,.45,.94) .5s;transition:opacity cubic-bezier(0.25,.46,.45,.94) .5s}.paging-strategy-scroll .ap-mesa-loading-display.ng-enter{-webkit-transition-delay:.4s;transition-delay:.4s}.paging-strategy-paginate .ap-mesa-loading-display.ng-enter{-webkit-transition-delay:0s;transition-delay:0s}.ap-mesa-loading-display.ng-enter,.ap-mesa-loading-display.ng-enter.ng-leave.ng-leave-active{opacity:0}.ap-mesa-loading-display.ng-enter.ng-enter-active,.ap-mesa-loading-display.ng-enter.ng-leave{opacity:1}.ap-mesa-status-display-wrapper.has-rows .ap-mesa-status-display{position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ap-mesa-error-display-inner{color:red;display:inline-block}.ap-mesa-status-display-inner,.ap-mesa-status-display-inner:after,.ap-mesa-status-display-inner:before{border-radius:50%;width:1.5em;height:1.5em;-webkit-animation:load7 1.8s infinite ease-in-out;animation:load7 1.8s infinite ease-in-out}.ap-mesa-status-display-inner{color:#fff;font-size:10px;margin:0 auto;position:relative;text-indent:-9999em;-webkit-transform:translateZ(0) translateY(-100%);-ms-transform:translateZ(0) translateY(-100%);transform:translateZ(0) translateY(-100%);-webkit-animation-delay:-.16s;animation-delay:-.16s}.ap-mesa-status-display-inner:after,.ap-mesa-status-display-inner:before{content:'';position:absolute;top:0}.ap-mesa-status-display-inner:before{left:-3.5em;-webkit-animation-delay:-.32s;animation-delay:-.32s}.ap-mesa-status-display-inner:after{left:3.5em}@-webkit-keyframes load7{0%,100%,80%{-webkit-box-shadow:0 1.5em 0 -1.3em;box-shadow:0 1.5em 0 -1.3em}40%{-webkit-box-shadow:0 1.5em 0 0;box-shadow:0 1.5em 0 0}}@keyframes load7{0%,100%,80%{-webkit-box-shadow:0 1.5em 0 -1.3em;box-shadow:0 1.5em 0 -1.3em}40%{-webkit-box-shadow:0 1.5em 0 0;box-shadow:0 1.5em 0 0}}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angularjs-table",
3 | "version": "2.21.5",
4 | "license": "Apache License, v2.0",
5 | "dependencies": {
6 | "angular": "^1.6",
7 | "jquery": "~2",
8 | "angular-sanitize": "~1.3"
9 | },
10 | "repository": "https://github.com/andyperlitch/angularjs-table",
11 | "types": "./angular-mesa.d.ts",
12 | "main": "dist/ap-mesa.js",
13 | "style": "dist/ap-mesa.css",
14 | "devDependencies": {
15 | "chai": "^1.9.1",
16 | "grunt": "^1.0.1",
17 | "grunt-autoprefixer": "~0.4.0",
18 | "grunt-bower-install": "~0.7.0",
19 | "grunt-concurrent": "~0.4.1",
20 | "grunt-contrib-clean": "~0.5.0",
21 | "grunt-contrib-coffee": "~0.7.0",
22 | "grunt-contrib-compass": "~0.6.0",
23 | "grunt-contrib-concat": "~0.3.0",
24 | "grunt-contrib-connect": "~0.5.0",
25 | "grunt-contrib-copy": "~0.4.1",
26 | "grunt-contrib-cssmin": "~0.7.0",
27 | "grunt-contrib-htmlmin": "~0.1.3",
28 | "grunt-contrib-jshint": "~0.7.1",
29 | "grunt-contrib-uglify": "~0.2.0",
30 | "grunt-contrib-watch": "~0.5.2",
31 | "grunt-google-cdn": "~0.2.0",
32 | "grunt-html2js": "^0.2.4",
33 | "grunt-injector": "^0.5.4",
34 | "grunt-karma": "^0.12.2",
35 | "grunt-newer": "~0.5.4",
36 | "grunt-ngmin": "~0.0.2",
37 | "grunt-rev": "~0.1.0",
38 | "grunt-svgmin": "~0.2.0",
39 | "grunt-usemin": "~2.0.0",
40 | "jshint-stylish": "~0.1.3",
41 | "karma": "^1.6.0",
42 | "karma-chai": "^0.1.0",
43 | "karma-chrome-launcher": "^0.1.2",
44 | "karma-coverage": "^0.2.7",
45 | "karma-firefox-launcher": "^0.1.3",
46 | "karma-mocha": "^0.1.3",
47 | "karma-ng-html2js-preprocessor": "^0.1.0",
48 | "karma-phantomjs-launcher": "^1.0.4",
49 | "karma-sinon": "^1.0.4",
50 | "karma-sinon-chai": "^0.1.5",
51 | "load-grunt-tasks": "~0.2.0",
52 | "mocha": "^1.18.2",
53 | "sinon": "^1.12.2",
54 | "time-grunt": "~0.2.1"
55 | },
56 | "engines": {
57 | "node": ">=0.8.0"
58 | },
59 | "scripts": {
60 | "test": "grunt",
61 | "gh-pages": "bash update-gh-pages.sh"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/ap-mesa.css:
--------------------------------------------------------------------------------
1 | /* main root element */
2 | .ap-mesa-wrapper {
3 | position: relative;
4 | }
5 |
6 | /* styles for both header- and rows- tables */
7 | .ap-mesa {
8 | table-layout: fixed;
9 | width: 100%;
10 | margin-bottom: 0;
11 | }
12 |
13 | /* the visible table header */
14 | .mesa-header-table {
15 | border-bottom: none;
16 | }
17 | .mesa-header-table thead > tr > th {
18 | border-width: 1px;
19 | }
20 |
21 | /* the invisible table header; used for correct column widths */
22 | .mesa-rows-table thead {
23 | height: 0;
24 | visibility: hidden;
25 | }
26 | .mesa-rows-table > thead > tr > th{
27 | border-width: 0;
28 | padding: 0 !important;
29 | }
30 | .mesa-rows-table-wrapper {
31 | overflow: auto;
32 | }
33 | .mesa-rows-table > tbody + tbody {
34 | border-top: none;
35 | }
36 |
37 | /* general styles for all cells in both tables */
38 | .ap-mesa th {
39 | white-space: nowrap;
40 | position: relative;
41 | }
42 | .ap-mesa td {
43 | word-wrap: break-word;
44 | overflow: hidden;
45 | }
46 |
47 | /* this type of cell is used to show messages like "loading" */
48 | .ap-mesa td.space-holder-row-cell {
49 | text-align: center;
50 | }
51 |
52 | /* search input */
53 | .ap-mesa tr.ap-mesa-filter-row td input {
54 | width: 100%;
55 | border-radius: 2em;
56 | border: 1px solid #CCC;
57 | outline: none;
58 | text-indent: 0.3em;
59 | font-size: 90%;
60 | }
61 | .ap-mesa tr.ap-mesa-filter-row td {
62 | position: relative;
63 | }
64 | /* button to clear search */
65 | .ap-mesa tr.ap-mesa-filter-row td .clear-search-btn {
66 | position: absolute;
67 | border-radius: 50%;
68 | border: none;
69 | right: 1em;
70 | top: 50%;
71 | transform: translateY(-50%);
72 | font-size: 12px;
73 | opacity: 0.2;
74 | color: white;
75 | background-color: black;
76 | padding: 0;
77 | width: 15px;
78 | line-height: 15px;
79 | }
80 |
81 | /* activated search input, as in when there is text in it */
82 | .ap-mesa tr.ap-mesa-filter-row td input.active {
83 | background-color: #3D82C2;
84 | color: #FFF;
85 | border-color: #747474;
86 | }
87 |
88 | /* placeholder object for when columns are being sorted */
89 | .ap-mesa .ap-mesa-column-placeholder {
90 | background-color: #DDD;
91 | }
92 |
93 | /* handle to grab in order to resize a column */
94 | .ap-mesa th .column-resizer {
95 | position: absolute;
96 | top:0;
97 | right: 0;
98 | width: 5px;
99 | height: 100%;
100 | border-width: 0 1px;
101 | cursor: col-resize;
102 | border-color: #DDD;
103 | border-style: solid;
104 | }
105 |
106 | /* this class is applied to a .column-resizer
107 | when a discreet width has been set on it */
108 | .ap-mesa th .column-resizer.discreet-width {
109 | background-color: #DDD;
110 | }
111 |
112 | /* wrapper for text in a th */
113 | .ap-mesa th .column-text {
114 | max-width: 100%;
115 | overflow: hidden;
116 | display: block;
117 | }
118 |
119 | /* the element showing what the new width will be */
120 | .ap-mesa th .column-resizer-marquee {
121 | left: 0;
122 | top: 0;
123 | height: 100%;
124 | border: 1px dotted #DEDEDE;
125 | position: absolute;
126 | }
127 |
128 | /* sorting icons */
129 | .ap-mesa th span.sorting-icon {
130 | font-size: 10px;
131 | }
132 | .ap-mesa th span.sort-priority {
133 | background-color: black;
134 | color: white;
135 | font-size: 9px;
136 | font-weight: bold;
137 | vertical-align: top;
138 | display: inline-block;
139 | height: 12px;
140 | width: 12px;
141 | text-align: center;
142 | border-radius: 50%;
143 | }
144 |
145 | .ap-mesa th span.glyphicon-sort {
146 | opacity: 0.2;
147 | }
148 | .ap-mesa th.sortable-column {
149 | cursor: pointer;
150 | user-select: none
151 | }
152 |
153 | /* dummy row */
154 | .ap-mesa-dummy-row {
155 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAlCAYAAACDKIOpAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABtJREFUeNpiuHv37n+G////MzAxAMHQIQACDAC7twbaN2nkgwAAAABJRU5ErkJggg==');
156 | background-repeat: repeat;
157 | }
158 | table tbody .ap-mesa-dummy-row td {
159 | border-top: none;
160 | padding: 0;
161 | }
162 | .ap-mesa-pagination {
163 | user-select: none;
164 | overflow: hidden;
165 | }
166 | .ap-mesa-pagination .rows-per-page-ctrl {
167 | float: right;
168 | }
169 | .ap-mesa-pagination .rows-per-page-ctrl .pagination {
170 | vertical-align: middle;
171 | }
172 | .ap-mesa-pagination .rows-per-page-msg {
173 | vertical-align: middle;
174 | }
175 | .ap-mesa-pagination ul.pagination li a {
176 | cursor: pointer;
177 | }
178 |
179 | /* async loader styles */
180 | .ap-mesa-status-display-wrapper {
181 | position: relative;
182 | text-align: center;
183 | }
184 | .ap-mesa-status-display {
185 | background: rgba(0,0,0,0.3);
186 | border-radius: 1rem;
187 | width: 20%;
188 | max-width: 130px;
189 | min-width: 100px;
190 | height: auto;
191 | padding: 0.5rem 1rem;
192 | position: relative;
193 | display: inline-block;
194 | margin-top: 0.7rem;
195 | }
196 | .ap-mesa-loading-display.ng-enter {
197 | transition: opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
198 | }
199 | .paging-strategy-scroll .ap-mesa-loading-display.ng-enter {
200 | transition-delay: 0.4s;
201 | }
202 | .paging-strategy-paginate .ap-mesa-loading-display.ng-enter {
203 | transition-delay: 0s;
204 | }
205 | .ap-mesa-loading-display.ng-enter,
206 | .ap-mesa-loading-display.ng-enter.ng-leave.ng-leave-active {
207 | opacity:0;
208 | }
209 |
210 | .ap-mesa-loading-display.ng-enter.ng-leave,
211 | .ap-mesa-loading-display.ng-enter.ng-enter-active {
212 | opacity:1;
213 | }
214 |
215 | .ap-mesa-status-display-wrapper.has-rows .ap-mesa-status-display {
216 | position: absolute;
217 | transform: translateX(-50%);
218 | }
219 |
220 | .ap-mesa-error-display-inner {
221 | color: red;
222 | display: inline-block;
223 | }
224 |
225 | /* credit to: https://projects.lukehaas.me/css-loaders/ */
226 | .ap-mesa-status-display-inner,
227 | .ap-mesa-status-display-inner:before,
228 | .ap-mesa-status-display-inner:after {
229 | border-radius: 50%;
230 | width: 1.5em;
231 | height: 1.5em;
232 | -webkit-animation-fill-mode: both;
233 | animation-fill-mode: both;
234 | -webkit-animation: load7 1.8s infinite ease-in-out;
235 | animation: load7 1.8s infinite ease-in-out;
236 | }
237 | .ap-mesa-status-display-inner {
238 | color: #ffffff;
239 | font-size: 10px;
240 | margin: 0 auto;
241 | position: relative;
242 | text-indent: -9999em;
243 | -webkit-transform: translateZ(0) translateY(-100%);
244 | -ms-transform: translateZ(0) translateY(-100%);
245 | transform: translateZ(0) translateY(-100%);
246 | -webkit-animation-delay: -0.16s;
247 | animation-delay: -0.16s;
248 | }
249 | .ap-mesa-status-display-inner:before,
250 | .ap-mesa-status-display-inner:after {
251 | content: '';
252 | position: absolute;
253 | top: 0;
254 | }
255 | .ap-mesa-status-display-inner:before {
256 | left: -3.5em;
257 | -webkit-animation-delay: -0.32s;
258 | animation-delay: -0.32s;
259 | }
260 | .ap-mesa-status-display-inner:after {
261 | left: 3.5em;
262 | }
263 | @-webkit-keyframes load7 {
264 | 0%,
265 | 80%,
266 | 100% {
267 | box-shadow: 0 1.5em 0 -1.3em;
268 | }
269 | 40% {
270 | box-shadow: 0 1.5em 0 0;
271 | }
272 | }
273 | @keyframes load7 {
274 | 0%,
275 | 80%,
276 | 100% {
277 | box-shadow: 0 1.5em 0 -1.3em;
278 | }
279 | 40% {
280 | box-shadow: 0 1.5em 0 0;
281 | }
282 | }
283 |
284 |
--------------------------------------------------------------------------------
/src/ap-mesa.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa', [
19 | 'apMesa.templates',
20 | 'ui.sortable',
21 | 'ngSanitize',
22 | 'apMesa.directives.apMesa'
23 | ]);
24 |
--------------------------------------------------------------------------------
/src/controllers/ApMesaController.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.controllers.ApMesaController', [
19 | 'apMesa.services.apMesaSortFunctions',
20 | 'apMesa.services.apMesaFilterFunctions',
21 | 'apMesa.services.apMesaFormatFunctions'
22 | ])
23 |
24 | .controller('ApMesaController',
25 | ['$scope','$element','apMesaFormatFunctions','apMesaSortFunctions','apMesaFilterFunctions','$log', '$window', '$filter', '$timeout', '$q', function($scope, $element, formats, sorts, filters, $log, $window, $filter, $timeout, $q) {
26 | var CONSTANTS = {
27 | minWidth: 40
28 | };
29 | // SCOPE FUNCTIONS
30 | $scope.getSelectableRows = function() {
31 | var tableRowFilter = $filter('apMesaRowFilter');
32 | return angular.isArray($scope.rows) ? tableRowFilter($scope.rows, $scope.columns, $scope.persistentState, $scope.transientState) : [];
33 | };
34 |
35 | $scope.isSelectedAll = function() {
36 | if (!angular.isArray($scope.rows) || ! angular.isArray($scope.selected)) {
37 | return false;
38 | }
39 | var rows = $scope.getSelectableRows();
40 | return (rows.length > 0 && rows.length === $scope.selected.length );
41 | };
42 |
43 | $scope.selectAll = function() {
44 | $scope.deselectAll();
45 | // Get a list of filtered rows
46 | var rows = $scope.getSelectableRows();
47 | if (rows.length <= 0) return;
48 | var columns = $scope.columns;
49 | var selectorKey = null;
50 | var selectObject = null;
51 | // Search for selector key in selector column
52 | for (var i=0; i< columns.length; i++) {
53 | if (columns[i].selector) {
54 | selectorKey = columns[i].key;
55 | selectObject = columns[i].selectObject;
56 | break;
57 | }
58 | }
59 | // Verify that selectorKey was found
60 | if (!selectorKey) {
61 | throw new Error('Unable to find selector column key for selectAll');
62 | }
63 | //select key or entire object from all rows
64 | for ( var i = 0; i < rows.length; i++) {
65 | $scope.selected.push(selectObject ? rows[i] : rows[i][selectorKey]);
66 | }
67 | };
68 |
69 | $scope.deselectAll = function() {
70 | while($scope.selected.length > 0) {
71 | $scope.selected.pop();
72 | }
73 | };
74 |
75 | $scope.toggleSelectAll = function($event) {
76 | var checkbox = $event.target;
77 | if (checkbox.checked) {
78 | $scope.selectAll();
79 | } else {
80 | $scope.deselectAll();
81 | }
82 | };
83 |
84 | function findSortItemIndex(id) {
85 | var sortLen = $scope.persistentState.sortOrder.length;
86 | for (var i = 0; i < sortLen; i++) {
87 | if ($scope.persistentState.sortOrder[i].id === id) {
88 | return i;
89 | }
90 | }
91 | }
92 |
93 | function findSortItem(id) {
94 | var index = findSortItemIndex(id);
95 | if (index > -1) {
96 | return $scope.persistentState.sortOrder[index];
97 | }
98 | }
99 |
100 | $scope.addSort = function(id, dir) {
101 | var sortItem = findSortItem(id);
102 | if (sortItem) {
103 | sortItem.dir = dir;
104 | } else {
105 | $scope.persistentState.sortOrder.push({
106 | id: id,
107 | dir: dir
108 | });
109 | }
110 | };
111 | $scope.removeSort = function(id) {
112 | var idx = findSortItemIndex(id);
113 | if (idx !== -1) {
114 | $scope.persistentState.sortOrder.splice(idx, 1);
115 | }
116 | };
117 | $scope.clearSort = function() {
118 | $scope.persistentState.sortOrder = [];
119 | };
120 | // Checks if columns have any filter fileds
121 | $scope.hasFilterFields = function() {
122 | if (!$scope.columns) {
123 | return false;
124 | }
125 | for (var i = $scope.columns.length - 1; i >= 0; i--) {
126 | if (typeof $scope.columns[i].filter !== 'undefined') {
127 | return true;
128 | }
129 | }
130 | return false;
131 | };
132 | // Clears search field for column, focus on input
133 | $scope.clearAndFocusSearch = function(columnId) {
134 | $scope.persistentState.searchTerms[columnId] = '';
135 | $element.find('tr.ap-mesa-filter-row th.column-' + columnId + ' input').focus();
136 | };
137 | // Toggles column sorting
138 | $scope.toggleSort = function($event, column) {
139 |
140 | // check if even sortable
141 | if (!column.sort) {
142 | return;
143 | }
144 |
145 | // check for existing sort on this column
146 | var sortItem = findSortItem(column.id);
147 |
148 | if ( $event.shiftKey ) {
149 | // shift is down, ignore other columns
150 | // but toggle between three states
151 | if (sortItem) {
152 | if (sortItem.dir === '+') {
153 | sortItem.dir = '-';
154 | } else if (sortItem.dir === '-') {
155 | $scope.removeSort(column.id);
156 | }
157 | } else {
158 | // Make ascending
159 | $scope.addSort(column.id, '+');
160 | }
161 |
162 | } else {
163 | // shift is not down, disable other
164 | // columns but toggle two states
165 | var lastState = sortItem ? sortItem.dir : '';
166 | $scope.clearSort();
167 | if (lastState === '+') {
168 | $scope.addSort(column.id, '-');
169 | }
170 | else {
171 | $scope.addSort(column.id, '+');
172 | }
173 |
174 | }
175 |
176 | $scope.saveToStorage();
177 | };
178 | // Retrieve className for given sorting state
179 | $scope.getSortClass = function(sorting) {
180 | var classes = $scope.options.sortClasses;
181 | if (sorting === '+') {
182 | return classes[1];
183 | }
184 | if (sorting === '-') {
185 | return classes[2];
186 | }
187 | return classes[0];
188 | };
189 | $scope.setColumns = function(columns) {
190 | try {
191 | $scope.columns = columns;
192 | var lookup = $scope.transientState.columnLookup = {};
193 | $scope.columns.forEach(function(column) {
194 | // formats
195 | var format = column.format;
196 | if (typeof format !== 'function') {
197 | if (typeof format === 'string') {
198 | if (typeof formats[format] === 'function') {
199 | column.format = formats[format];
200 | }
201 | else {
202 |
203 | try {
204 | column.format = $filter(format);
205 | } catch (e) {
206 | delete column.format;
207 | $log.warn('format function reference in column(id=' + column.id + ') ' +
208 | 'was not found in built-in format functions or $filters. ' +
209 | 'format function given: "' + format + '". ' +
210 | 'Available built-ins: ' + Object.keys(formats).join(',') + '. ' +
211 | 'If you supplied a $filter, ensure it is available on this module');
212 | }
213 |
214 | }
215 | } else {
216 | delete column.format;
217 | }
218 | }
219 |
220 | // async get
221 | if (!$scope.options.getData) {
222 | // sort
223 | var sort = column.sort;
224 | if (typeof sort !== 'function') {
225 | if (typeof sort === 'string') {
226 | if (typeof sorts[sort] === 'function') {
227 | column.sort = sorts[sort](column.key);
228 | }
229 | else {
230 | delete column.sort;
231 | $log.warn('sort function reference in column(id=' + column.id + ') ' +
232 | 'was not found in built-in sort functions. ' +
233 | 'sort function given: "' + sort + '". ' +
234 | 'Available built-ins: ' + Object.keys(sorts).join(',') + '. ');
235 | }
236 | } else {
237 | delete column.sort;
238 | }
239 | }
240 | // filter
241 | var filter = column.filter;
242 | if (typeof filter !== 'function') {
243 | if (typeof filter === 'string') {
244 | if (typeof filters[filter] === 'function') {
245 | column.filter = filters[filter];
246 | }
247 | else {
248 | delete column.filter;
249 | $log.warn('filter function reference in column(id=' + column.id + ') ' +
250 | 'was not found in built-in filter functions. ' +
251 | 'filter function given: "' + filter + '". ' +
252 | 'Available built-ins: ' + Object.keys(filters).join(',') + '. ');
253 | }
254 | } else {
255 | delete column.filter;
256 | }
257 | }
258 | }
259 |
260 | // populate lookup
261 | lookup[column.id] = column;
262 |
263 | });
264 |
265 | // check enabledColumns for validity
266 | if (angular.isArray($scope.enabledColumns)) {
267 | // if any of the ids in enabledColumns do not map to a column in the new column set...
268 | if ($scope.enabledColumns.some(function(columnId) {
269 | return !lookup[columnId];
270 | })) {
271 | // ...unset the enabled columns
272 | $scope.enabledColumns = undefined;
273 | }
274 | } else {
275 | $scope.enabledColumns = $scope.columns.map(function(column) {
276 | return column.id;
277 | });
278 | }
279 | } catch (e) {
280 | console.log(e.message);
281 | }
282 | };
283 |
284 | $scope.startColumnResize = function($event, column) {
285 |
286 | // Stop default so text does not get selected
287 | $event.preventDefault();
288 | $event.originalEvent.preventDefault();
289 | $event.stopPropagation();
290 |
291 | // init variable for new width
292 | var new_width = false;
293 |
294 | // store initial mouse position
295 | var initial_x = $event.pageX;
296 |
297 | // create marquee element
298 | var $m = $('
');
299 |
300 | // append to th
301 | var $th = $($event.target).parent('th');
302 | $th.append($m);
303 |
304 | // set initial marquee dimensions
305 | var initial_width = $th.outerWidth();
306 |
307 | function mousemove(e) {
308 | // calculate changed width
309 | var current_x = e.pageX;
310 | var diff = current_x - initial_x;
311 | new_width = initial_width + diff;
312 |
313 | // update marquee dimensions
314 | $m.css('width', new_width + 'px');
315 | }
316 |
317 | $m.css({
318 | width: initial_width + 'px',
319 | height: $th.outerHeight() + 'px'
320 | });
321 |
322 | // set mousemove listener
323 | $($window).on('mousemove', mousemove);
324 |
325 | // set mouseup/mouseout listeners
326 | $($window).one('mouseup', function(e) {
327 | e.stopPropagation();
328 | // remove marquee, remove window mousemove listener
329 | $m.remove();
330 | $($window).off('mousemove', mousemove);
331 |
332 | // set new width on th
333 | // if a new width was set
334 | if (new_width === false) {
335 | column.width = Math.max(initial_width, 0);
336 | } else {
337 | column.width = Math.max(new_width, CONSTANTS.minWidth);
338 | }
339 |
340 | $scope.$apply();
341 | });
342 | };
343 | $scope.sortableOptions = {
344 | axis: 'x',
345 | handle: '.column-text',
346 | helper: 'clone',
347 | placeholder: 'ap-mesa-column-placeholder',
348 | distance: 5,
349 | update: function() {
350 | // use of $timeout req'd for this because the update event comes before
351 | // the model is updated!
352 | $timeout(function() {
353 | $scope.enabledColumns = $scope.enabledColumnObjects.map(function(column) { return column.id; });
354 | });
355 | }
356 | };
357 |
358 | $scope.getActiveColCount = function() {
359 | var count = 0;
360 | $scope.columns.forEach(function(col) {
361 | if (!col.disabled) {
362 | count++;
363 | }
364 | });
365 | return count;
366 | };
367 |
368 | $scope.saveToStorage = function() {
369 | if (!$scope.storage) {
370 | return;
371 | }
372 | // init object to stringify/save
373 | var state = {};
374 |
375 | // save state objects
376 | ['sortOrder', 'searchTerms'].forEach(function(prop) {
377 | state[prop] = $scope.persistentState[prop];
378 | });
379 |
380 | // save enabled columns (can't be in persistent state because it is a directive @Input)
381 | state.enabledColumns = $scope.enabledColumns;
382 |
383 | // save non-transient options
384 | state.options = {};
385 | ['rowLimit', 'pagingStrategy', 'storageHash'].forEach(function(prop){
386 | state.options[prop] = $scope.options[prop];
387 | });
388 |
389 | // Save to storage
390 | var valueToStore = $scope.options.stringifyStorage ? JSON.stringify(state) : state;
391 | $scope.storage.setItem($scope.storageKey, valueToStore);
392 | };
393 |
394 | $scope.loadFromStorage = function() {
395 |
396 | var options = $scope.options;
397 |
398 | if (!$scope.storage) {
399 | return;
400 | }
401 |
402 | // Attempt to parse the storage
403 | var stateValue = $scope.storage.getItem($scope.storageKey);
404 |
405 | $q.when(stateValue).then(function(stateStringOrObject) {
406 |
407 | if (!stateStringOrObject) {
408 | console.warn('angularjs-table: loading from storage failed because storage.getItem did not return anything.');
409 | return;
410 | }
411 |
412 | try {
413 |
414 | var state;
415 | if (options.stringifyStorage) {
416 | if (typeof stateStringOrObject !== 'string') {
417 | throw new TypeError('storage.getItem is expected to return a string if options.stringifyStorage is true.');
418 | }
419 | state = JSON.parse(stateStringOrObject);
420 | } else if (angular.isObject(stateStringOrObject)) {
421 | state = stateStringOrObject;
422 | } else {
423 | throw new TypeError('storage.getItem is expected to return an object if options.stringifyStorage is false.');
424 | }
425 |
426 | // if mimatched storage hash, stop loading from storage
427 | if (state.options.storageHash !== $scope.options.storageHash) {
428 | return;
429 | }
430 |
431 | // load state objects
432 | ['sortOrder', 'searchTerms'].forEach(function(prop){
433 | $scope.persistentState[prop] = state[prop];
434 | });
435 |
436 | // load enabled columns list
437 | $scope.enabledColumns = state.enabledColumns;
438 |
439 | // load options
440 | ['rowLimit', 'pagingStrategy', 'storageHash'].forEach(function(prop) {
441 | $scope.options[prop] = state.options[prop];
442 | });
443 |
444 | } catch (e) {
445 | console.warn('angularjs-table: failed to load state from storage. ', e);
446 | }
447 | }, function(e) {
448 | console.warn('angularjs-table: storage.getItem failed: ', e);
449 | });
450 | };
451 |
452 | }]);
453 |
--------------------------------------------------------------------------------
/src/directives/apMesaCell.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.directives.apMesaCell', [
19 | 'apMesa.directives.apMesaSelector'
20 | ])
21 |
22 | .directive('apMesaCell', function($compile) {
23 |
24 | function link(scope, element) {
25 | scope.$watch('column', function(column) {
26 | var cellMarkup = '';
27 | if (column.template) {
28 | cellMarkup = column.template;
29 | }
30 | else if (column.templateUrl) {
31 | cellMarkup = '
';
32 | }
33 | else if (column.selector === true) {
34 | cellMarkup = ' ';
35 | }
36 | else if (column.ngFilter) {
37 | cellMarkup = '{{ row[column.key] | ' + column.ngFilter + ':row }}';
38 | }
39 | else if (column.format) {
40 | var valueExpr = (scope.options !== undefined && {}.hasOwnProperty.call(scope.options, 'getter')) ? 'options.getter(column.key, row)' : 'row[column.key]';
41 | cellMarkup = '{{ column.format(' + valueExpr + ', row, column, options) }}';
42 | }
43 | else if(scope.options !== undefined && {}.hasOwnProperty.call(scope.options, 'getter')) {
44 |
45 | cellMarkup = '{{ options.getter(column.key, row) }}';
46 | }
47 | else {
48 | cellMarkup = '{{ row[column.key] }}';
49 | }
50 | element.html(cellMarkup);
51 | $compile(element.contents())(scope);
52 | });
53 | }
54 |
55 | return {
56 | scope: true,
57 | link: link
58 | };
59 | });
60 |
--------------------------------------------------------------------------------
/src/directives/apMesaDummyRows.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | /**
19 | * @ngdoc directive
20 | * @name apMesa.directive:apMesaDummyRows
21 | * @restrict A
22 | * @description inserts dummy s for non-rendered rows
23 | * @element tbody
24 | * @example
25 | **/
26 | angular.module('apMesa.directives.apMesaDummyRows', [])
27 | .directive('apMesaDummyRows', function() {
28 |
29 | return {
30 | template: ' ',
31 | scope: true,
32 | link: function(scope, element, attrs) {
33 |
34 | scope.$on('angular-mesa:update-dummy-rows', function() {
35 | var offsetRange = scope.$eval(attrs.apMesaDummyRows);
36 | var rowsHeight = (offsetRange[1] - offsetRange[0]) * scope.rowHeight;
37 | for (var k in scope.transientState.expandedRows) {
38 | var kInt = parseInt(k);
39 | if (kInt >= offsetRange[0] && kInt < offsetRange[1]) {
40 | rowsHeight += scope.transientState.expandedRowHeights[k];
41 | }
42 | }
43 | scope.dummyRowHeight = rowsHeight;
44 | });
45 |
46 | }
47 | };
48 |
49 | });
50 |
--------------------------------------------------------------------------------
/src/directives/apMesaExpandable.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.directives.apMesaExpandable', [])
4 | .directive('apMesaExpandable', ['$compile', function($compile) {
5 | return {
6 | scope: false,
7 | link: function(scope, element, attrs) {
8 | scope.$watch('row', function() {
9 | var innerEl;
10 | if (scope.options.expandableTemplateUrl) {
11 | innerEl = angular.element('
');
12 | } else if (scope.options.expandableTemplate) {
13 | innerEl = angular.element(scope.options.expandableTemplate);
14 | } else {
15 | return;
16 | }
17 | $compile(innerEl)(scope);
18 | element.html('');
19 | element.append(innerEl);
20 | });
21 | }
22 | };
23 | }]);
24 |
--------------------------------------------------------------------------------
/src/directives/apMesaPaginationCtrls.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.directives.apMesaPaginationCtrls', [])
4 | .directive('apMesaPaginationCtrls', function($timeout) {
5 |
6 | return {
7 | templateUrl: 'src/templates/apMesaPaginationCtrls.tpl.html',
8 | scope: true,
9 | link: function(scope, element) {
10 | function updatePageLinks() {
11 | var pageLinks = [];
12 | var numPages = Math.ceil(scope.transientState.filterCount / scope.options.rowsPerPage);
13 | var currentPage = scope.transientState.pageOffset;
14 | var maxPageLinks = Math.max(5, scope.options.maxPageLinks); // must be a minimum of 5 max page links
15 |
16 | if (numPages <= maxPageLinks) {
17 | for (var i = 0; i < numPages; i++) {
18 | pageLinks.push({
19 | gap: false,
20 | page: i,
21 | current: currentPage === i
22 | });
23 | }
24 | } else if (currentPage < (maxPageLinks - 3)) {
25 | for (var i = 0; i < maxPageLinks - 2; i++) {
26 | pageLinks.push({
27 | gap: false,
28 | page: i,
29 | current: currentPage === i
30 | });
31 | }
32 | pageLinks.push({
33 | gap: true,
34 | page: -1,
35 | current: false
36 | }, {
37 | gap: false,
38 | page: numPages - 1,
39 | current: false
40 | });
41 | } else if (numPages - currentPage <= (maxPageLinks - 3)) {
42 | pageLinks.push({
43 | gap: false,
44 | page: 0,
45 | current: false
46 | }, {
47 | gap: true,
48 | page: -1,
49 | current: false
50 | });
51 | var startingPage = numPages - (maxPageLinks - 2);
52 | for (var i = startingPage; i < numPages; i++) {
53 | pageLinks.push({
54 | gap: false,
55 | page: i,
56 | current: currentPage === i
57 | });
58 | }
59 | } else {
60 | pageLinks.push({
61 | gap: false,
62 | page: 0,
63 | current: false
64 | }, {
65 | gap: true,
66 | page: -1,
67 | current: false
68 | });
69 | var remainingLinkCount = maxPageLinks - 4;
70 | for (var i = 0; remainingLinkCount > 0; i++) {
71 | var distance = i % 2 ? (i + 1)/2 : -(i / 2);
72 | var page = currentPage + distance;
73 | if (distance >= 0) {
74 | pageLinks.push({
75 | gap: false,
76 | page: page,
77 | current: distance === 0
78 | });
79 | } else {
80 | pageLinks.splice(2, 0, {
81 | gap: false,
82 | page: page,
83 | current: false
84 | });
85 | }
86 | --remainingLinkCount;
87 | }
88 | pageLinks.push({
89 | gap: true,
90 | page: -1,
91 | current: false
92 | }, {
93 | gap: false,
94 | page: numPages - 1,
95 | current: false
96 | });
97 | }
98 | scope.pageLinks = pageLinks;
99 | scope.lastPage = numPages -1;
100 | }
101 | scope.$watch('transientState.filterCount', updatePageLinks);
102 | scope.$watch('options.rowsPerPage', updatePageLinks);
103 | scope.$watch('transientState.pageOffset', updatePageLinks);
104 |
105 | scope.goBack = function() {
106 | if (scope.transientState.pageOffset === 0) {
107 | return;
108 | }
109 | scope.transientState.pageOffset--;
110 | }
111 |
112 | scope.goForward = function() {
113 | if (scope.transientState.pageOffset === scope.lastPage) {
114 | return;
115 | }
116 | scope.transientState.pageOffset++;
117 | }
118 | }
119 | };
120 | });
121 |
--------------------------------------------------------------------------------
/src/directives/apMesaRow.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.directives.apMesaRow', ['apMesa.directives.apMesaCell'])
4 | .directive('apMesaRow', function($timeout) {
5 | return {
6 | template: ' ',
7 | scope: false,
8 | link: function(scope, element) {
9 | var index;
10 |
11 | if (scope.options.pagingStrategy === 'SCROLL') {
12 | index = scope.$index + scope.transientState.rowOffset;
13 | scope.rowIsExpanded = !!scope.transientState.expandedRows[index];
14 | } else if (scope.options.pagingStrategy === 'PAGINATE') {
15 | scope.$watch('options.rowsPerPage', function(rowsPerPage) {
16 | index = scope.$index + (scope.transientState.pageOffset * rowsPerPage);
17 | scope.rowIsExpanded = !!scope.transientState.expandedRows[index];
18 | });
19 | }
20 |
21 | scope.$watch('transientState.expandedRows', function(nv, ov) {
22 | if (nv !== ov) {
23 | scope.rowIsExpanded = false;
24 | }
25 | });
26 |
27 | scope.toggleRowExpand = function() {
28 | scope.transientState.expandedRows[index] = scope.rowIsExpanded = !scope.transientState.expandedRows[index];
29 | if (!scope.transientState.expandedRows[index]) {
30 | delete scope.transientState.expandedRows[index];
31 | delete scope.transientState.expandedRowHeights[index];
32 | } else {
33 | scope.refreshExpandedHeight(false);
34 | }
35 | };
36 | scope.refreshExpandedHeight = function(fromTemplate) {
37 | $timeout(function() {
38 | var newHeight = element.next('tr.ap-mesa-expand-panel').height();
39 | scope.transientState.expandedRowHeights[index] = newHeight;
40 | });
41 | };
42 | }
43 | };
44 | });
45 |
--------------------------------------------------------------------------------
/src/directives/apMesaRows.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.directives.apMesaRows',[
19 | 'apMesa.directives.apMesaRow',
20 | 'apMesa.filters.apMesaRowFilter',
21 | 'apMesa.filters.apMesaRowSorter',
22 | 'apMesa.services.apMesaDebounce'
23 | ])
24 |
25 | .directive('apMesaRows', ['$filter', '$timeout', 'apMesaDebounce', '$rootScope', function($filter, $timeout, debounce, $rootScope) {
26 |
27 | var tableRowFilter = $filter('apMesaRowFilter');
28 | var tableRowSorter = $filter('apMesaRowSorter');
29 | var limitTo = $filter('limitTo');
30 |
31 | /**
32 | * Updates the visible_rows array on the scope synchronously.
33 | * @param {ng.IScope} scope The scope of the particular apMesaRows instance.
34 | * @return {void}
35 | */
36 | function updateVisibleRows(scope) {
37 |
38 | // sanity check
39 | if (!scope.rows || !scope.enabledColumnObjects) {
40 | return [];
41 | }
42 |
43 | // scope.rows
44 | var visible_rows, idx;
45 |
46 | // filter rows
47 | visible_rows = tableRowFilter(scope.rows, scope.enabledColumnObjects, scope.persistentState, scope.transientState, scope.options);
48 |
49 | // sort rows
50 | visible_rows = tableRowSorter(visible_rows, scope.enabledColumnObjects, scope.persistentState.sortOrder, scope.options, scope.transientState);
51 |
52 | // limit rows
53 | if (scope.options.pagingStrategy === 'SCROLL') {
54 | visible_rows = limitTo(visible_rows, Math.floor(scope.transientState.rowOffset) - scope.transientState.filterCount);
55 | visible_rows = limitTo(visible_rows, scope.persistentState.rowLimit + Math.ceil(scope.transientState.rowOffset % 1));
56 | idx = scope.transientState.rowOffset;
57 | } else if (scope.options.pagingStrategy === 'PAGINATE') {
58 | var pagedRowOffset = scope.transientState.pageOffset * scope.persistentState.rowLimit;
59 | visible_rows = visible_rows.slice(pagedRowOffset, pagedRowOffset + scope.persistentState.rowLimit);
60 | idx = pagedRowOffset;
61 | }
62 |
63 | // add index to each row
64 | visible_rows.forEach(function(row) {
65 | row.$$$index = idx++;
66 | });
67 |
68 | scope.visible_rows = visible_rows;
69 | scope.$broadcast('angular-mesa:update-dummy-rows');
70 | }
71 |
72 | /**
73 | * Updates the visible_rows array on the scope asynchronously, using the options.getData function (when present).
74 | * This gets passed to debounce in the link function.
75 | * @param {ng.IScope} scope The scope of the particular apMesaRows instance
76 | * @return {ng.IPromise} Returns the promise of the request
77 | */
78 | function UpdateVisibleRowsAsync(scope) {
79 | // get offset
80 | var offset;
81 | if (scope.options.pagingStrategy === 'SCROLL') {
82 | offset = scope.transientState.rowOffset;
83 | } else if (scope.options.pagingStrategy === 'PAGINATE') {
84 | offset = scope.transientState.pageOffset * scope.persistentState.rowLimit;
85 | }
86 |
87 | // get active filter
88 | var searchTerms = scope.persistentState.searchTerms;
89 | var activeFilters = scope.enabledColumnObjects
90 | .filter(function(column) {
91 | return !! searchTerms[column.id];
92 | })
93 | .map(function(column) {
94 | return { column: column, value: searchTerms[column.id] };
95 | });
96 |
97 | // get active sorts
98 | var enabledColumnLookup = {};
99 | scope.enabledColumns.forEach(function(id) {
100 | enabledColumnLookup[id] = true;
101 | });
102 | var activeSorts = scope.persistentState.sortOrder
103 | .filter(function(sortItem) {
104 | return enabledColumnLookup[sortItem.id];
105 | })
106 | .map(function(sortItem) {
107 | return {
108 | column: scope.transientState.columnLookup[sortItem.id],
109 | direction: sortItem.dir === '+' ? 'ASC' : 'DESC'
110 | };
111 | });
112 |
113 | scope.transientState.loadingError = false;
114 | scope.api.setLoading(true);
115 | var getDataPromise = scope.transientState.getDataPromise = scope.options.getData(
116 | offset,
117 | scope.persistentState.rowLimit,
118 | activeFilters,
119 | activeSorts
120 | ).then(function(res) {
121 | if (getDataPromise !== scope.transientState.getDataPromise) {
122 | // new request made, cancelling this one
123 | return;
124 | }
125 | var total = res.total;
126 | var rows = res.rows;
127 | var i = offset;
128 | scope.transientState.rowOffset = offset;
129 | scope.transientState.filterCount = total;
130 | scope.visible_rows = rows;
131 | rows.forEach(function(row) {
132 | row.$$$index = i++;
133 | });
134 | scope.transientState.getDataPromise = null;
135 | scope.api.setLoading(false);
136 | scope.getDataError = undefined;
137 | scope.$emit('angular-mesa:update-dummy-rows');
138 | }, function(e) {
139 | scope.transientState.getDataPromise = null;
140 | scope.transientState.loadingError = true;
141 | scope.getDataError = e;
142 | scope.api.setLoading(false);
143 | });
144 | }
145 |
146 | function link(scope) {
147 |
148 | var updateVisibleRowsAsync = debounce(UpdateVisibleRowsAsync, 200, { leading: false, trailing: true })
149 |
150 | var updateHandler = function(newValue, oldValue) {
151 | if (newValue === oldValue) {
152 | return;
153 | }
154 | if (!scope.options.getData) {
155 | updateVisibleRows(scope);
156 | } else {
157 | updateVisibleRowsAsync(scope);
158 | }
159 |
160 | scope.transientState.expandedRows = {};
161 | };
162 |
163 | var updateHandlerWithoutClearingCollapsed = function(newValue, oldValue) {
164 | if (newValue === oldValue) {
165 | return;
166 | }
167 | if (!scope.options.getData) {
168 | updateVisibleRows(scope);
169 | } else {
170 | updateVisibleRowsAsync(scope);
171 | }
172 |
173 | };
174 |
175 | // Watchers that trigger updates to visible rows
176 | scope.$watch('persistentState.searchTerms', function(nv, ov) {
177 | if (!angular.equals(nv, ov)) {
178 | scope.resetOffset();
179 | }
180 | updateHandler(nv, ov);
181 | }, true);
182 | scope.$watch('persistentState.sortOrder', function(nv, ov) {
183 | if (!angular.equals(nv, ov)) {
184 | scope.resetOffset();
185 | }
186 | updateHandler(nv, ov);
187 | }, true);
188 | scope.$watch('transientState.rowOffset', function(nv, ov) {
189 | if (scope.options.pagingStrategy === 'SCROLL') {
190 | updateHandlerWithoutClearingCollapsed(nv, ov);
191 | }
192 | });
193 | scope.$watch('persistentState.rowLimit', function(nv, ov) {
194 | updateHandlerWithoutClearingCollapsed(nv, ov);
195 | });
196 | scope.$watch('transientState.pageOffset', function(nv, ov) {
197 | updateHandlerWithoutClearingCollapsed(nv, ov);
198 | });
199 | scope.$watch('transientState.filterCount', function(nv, ov) {
200 | if (!scope.options.getData) {
201 | updateHandler(nv, ov);
202 | }
203 | });
204 | scope.$watch('rows', function(newRows) {
205 | if (angular.isArray(newRows)) {
206 | updateHandler(true, false);
207 | }
208 | });
209 | scope.$watch('enabledColumnObjects', function(nv, ov) {
210 | updateHandler(nv, ov);
211 | });
212 | scope.$watch('options.getData', function(getData) {
213 | if (angular.isFunction(getData)) {
214 | updateHandler(true, false);
215 | }
216 | });
217 | scope.$on('apMesa:forceRefresh', function() {
218 | updateHandler(true, false);
219 | });
220 | }
221 |
222 | return {
223 | restrict: 'A',
224 | templateUrl: 'src/templates/apMesaRows.tpl.html',
225 | compile: function(tElement, tAttrs) {
226 | var tr = tElement.find('tr[ng-repeat-start]');
227 | var repeatString = tr.attr('ng-repeat-start');
228 | repeatString += tAttrs.trackBy ? ' track by row[options.trackBy]' : ' track by row.$$$index';
229 | tr.attr('ng-repeat-start', repeatString);
230 | if (tAttrs.onRowClick) {
231 | tElement.find('tr[ng-repeat-start]').attr('ng-click', tAttrs.onRowClick)
232 | }
233 | return link;
234 | }
235 | };
236 | }]);
237 |
--------------------------------------------------------------------------------
/src/directives/apMesaSelector.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.directives.apMesaSelector', [])
19 |
20 | .directive('apMesaSelector', function() {
21 | return {
22 | restrict: 'A',
23 | scope: false,
24 | link: function postLink(scope, element) {
25 | var selected = scope.selected;
26 | var row = scope.row;
27 | var column = scope.column;
28 | element.on('click', function() {
29 |
30 | // Retrieve position in selected list
31 | var idx = selected.indexOf(column.selectObject ? row : row[column.key]);
32 |
33 | // it is selected, deselect it:
34 | if (idx >= 0) {
35 | selected.splice(idx,1);
36 | }
37 |
38 | // it is not selected, push to list
39 | else {
40 | selected.push(column.selectObject ? row : row[column.key]);
41 | }
42 | scope.$apply();
43 | });
44 | }
45 | };
46 | });
47 |
--------------------------------------------------------------------------------
/src/directives/apMesaStatusDisplay.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.directives.apMesaStatusDisplay', [])
4 |
5 | .directive('apMesaStatusDisplay', function() {
6 | return {
7 | replace: true,
8 | templateUrl: 'src/templates/apMesaStatusDisplay.tpl.html',
9 | };
10 | });
11 |
--------------------------------------------------------------------------------
/src/directives/apMesaThTitle.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('apMesa.directives.apMesaThTitle', [])
4 | .directive('apMesaThTitle', function($compile) {
5 | function link(scope, element) {
6 | var column = scope.column;
7 | var template = '{{ column.id }} ';
8 | if (angular.isString(column.labelTemplateUrl)) {
9 | template = ' ';
10 | } else if (angular.isString(column.labelTemplate)) {
11 | template = '' + column.labelTemplate + ' ';
12 | } else if (angular.isString(column.label)) {
13 | template = '{{ column.label }} ';
14 | }
15 | element.html(template);
16 | $compile(element.contents())(scope);
17 | }
18 | return { link: link };
19 | });
20 |
--------------------------------------------------------------------------------
/src/filters/apMesaRowFilter.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.filters.apMesaRowFilter',[
19 | 'apMesa.services.apMesaFilterFunctions'
20 | ])
21 |
22 | .filter('apMesaRowFilter', ['apMesaFilterFunctions', '$log', function(tableFilterFunctions, $log) {
23 | return function tableRowFilter(rows, columns, persistentState, transientState, options) {
24 |
25 | var enabledFilterColumns, result = rows;
26 |
27 | // gather enabled filter functions
28 | enabledFilterColumns = columns.filter(function(column) {
29 | // check search term
30 | var term = persistentState.searchTerms[column.id];
31 | if (typeof term === 'string') {
32 |
33 | // filter empty strings and whitespace
34 | if (!term.trim()) {
35 | return false;
36 | }
37 |
38 | // check search filter function
39 | if (typeof column.filter === 'function') {
40 | return true;
41 | }
42 | // not a function, check for predefined filter function
43 | var predefined = tableFilterFunctions[column.filter];
44 | if (typeof predefined === 'function') {
45 | column.filter = predefined;
46 | return true;
47 | }
48 | $log.warn('apMesa: The filter function "'+column.filter+'" ' +
49 | 'specified by column(id='+column.id+').filter ' +
50 | 'was not found in predefined tableFilterFunctions. ' +
51 | 'Available filters: "'+Object.keys(tableFilterFunctions).join('","')+'"');
52 | }
53 | return false;
54 | });
55 |
56 | // loop through rows and filter on every enabled function
57 | if (enabledFilterColumns.length) {
58 | result = rows.filter(function(row) {
59 | for (var i = enabledFilterColumns.length - 1; i >= 0; i--) {
60 | var col = enabledFilterColumns[i];
61 | var filter = col.filter;
62 | var term = persistentState.searchTerms[col.id];
63 | var value = (options !== undefined && {}.hasOwnProperty.call(options, 'getter'))? options.getter(col.key, row):row[col.key];
64 | var computedValue = typeof col.format === 'function' ? col.format(value, row, col, options) : value;
65 | if (!filter(term, value, computedValue, row, col, options)) {
66 | return false;
67 | }
68 | }
69 | return true;
70 | });
71 | }
72 | transientState.filterCount = result.length;
73 | return result;
74 | };
75 | }]);
76 |
--------------------------------------------------------------------------------
/src/filters/apMesaRowSorter.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.filters.apMesaRowSorter', [])
19 |
20 | .filter('apMesaRowSorter', function() {
21 | return function tableRowSorter(rows, columns, sortOrder, options, transientState) {
22 | if (!sortOrder.length) {
23 | return rows;
24 | }
25 | var arrayCopy = rows.slice();
26 | var enabledColumns = {};
27 | columns.forEach(function(column) {
28 | enabledColumns[column.id] = true;
29 | });
30 | // js sort doesn't work as expected because it rearranges the equal elements
31 | // so we will arrange elements only if they are different, based on the element index
32 | var sortArray = arrayCopy.map(function (data, index) {
33 | return { index: index, data: data };
34 | });
35 |
36 | sortArray.sort(function (a, b) {
37 | for (var i = 0; i < sortOrder.length; i++) {
38 | var sortItem = sortOrder[i];
39 | if (!enabledColumns[sortItem.id]) {
40 | continue;
41 | }
42 | var column = transientState.columnLookup[sortItem.id];
43 | var dir = sortItem.dir;
44 | if (column && column.sort) {
45 | var fn = column.sort;
46 | var result = dir === '+' ? fn(a.data, b.data, options, column) : fn(b.data, a.data, options, column);
47 | if (result !== 0) {
48 | return result;
49 | }
50 | }
51 | }
52 | return a.index - b.index;
53 | });
54 |
55 | return sortArray.map(function (value) {
56 | return value.data;
57 | });
58 | };
59 | });
60 |
--------------------------------------------------------------------------------
/src/services/apMesaDebounce.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /** _.debounce, modified to use $timeout instead of setTimeout */
4 | angular.module('apMesa.services.apMesaDebounce', [])
5 |
6 | .factory('apMesaDebounce', ['$timeout', function($timeout) {
7 | /**
8 | * lodash (Custom Build)
9 | * Build: `lodash modularize exports="npm" -o ./`
10 | * Copyright jQuery Foundation and other contributors
11 | * Released under MIT license
12 | * Based on Underscore.js 1.8.3
13 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
14 | */
15 |
16 | /** Used as the `TypeError` message for "Functions" methods. */
17 | var FUNC_ERROR_TEXT = 'Expected a function';
18 |
19 | /** Used as references for various `Number` constants. */
20 | var NAN = 0 / 0;
21 |
22 | /** `Object#toString` result references. */
23 | var symbolTag = '[object Symbol]';
24 |
25 | /** Used to match leading and trailing whitespace. */
26 | var reTrim = /^\s+|\s+$/g;
27 |
28 | /** Used to detect bad signed hexadecimal string values. */
29 | var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
30 |
31 | /** Used to detect binary string values. */
32 | var reIsBinary = /^0b[01]+$/i;
33 |
34 | /** Used to detect octal string values. */
35 | var reIsOctal = /^0o[0-7]+$/i;
36 |
37 | /** Built-in method references without a dependency on `root`. */
38 | var freeParseInt = parseInt;
39 |
40 | /** Detect free variable `global` from Node.js. */
41 | var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
42 |
43 | /** Detect free variable `self`. */
44 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
45 |
46 | /** Used as a reference to the global object. */
47 | var root = freeGlobal || freeSelf || Function('return this')();
48 |
49 | /** Used for built-in method references. */
50 | var objectProto = Object.prototype;
51 |
52 | /**
53 | * Used to resolve the
54 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
55 | * of values.
56 | */
57 | var objectToString = objectProto.toString;
58 |
59 | /* Built-in method references for those with the same name as other `lodash` methods. */
60 | var nativeMax = Math.max,
61 | nativeMin = Math.min;
62 |
63 | /**
64 | * Gets the timestamp of the number of milliseconds that have elapsed since
65 | * the Unix epoch (1 January 1970 00:00:00 UTC).
66 | *
67 | * @static
68 | * @memberOf _
69 | * @since 2.4.0
70 | * @category Date
71 | * @returns {number} Returns the timestamp.
72 | * @example
73 | *
74 | * _.defer(function(stamp) {
75 | * console.log(_.now() - stamp);
76 | * }, _.now());
77 | * // => Logs the number of milliseconds it took for the deferred invocation.
78 | */
79 | var now = function() {
80 | return root.Date.now();
81 | };
82 |
83 | /**
84 | * Creates a debounced function that delays invoking `func` until after `wait`
85 | * milliseconds have elapsed since the last time the debounced function was
86 | * invoked. The debounced function comes with a `cancel` method to cancel
87 | * delayed `func` invocations and a `flush` method to immediately invoke them.
88 | * Provide `options` to indicate whether `func` should be invoked on the
89 | * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
90 | * with the last arguments provided to the debounced function. Subsequent
91 | * calls to the debounced function return the result of the last `func`
92 | * invocation.
93 | *
94 | * **Note:** If `leading` and `trailing` options are `true`, `func` is
95 | * invoked on the trailing edge of the timeout only if the debounced function
96 | * is invoked more than once during the `wait` timeout.
97 | *
98 | * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
99 | * until to the next tick, similar to `$timeout` with a timeout of `0`.
100 | *
101 | * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
102 | * for details over the differences between `_.debounce` and `_.throttle`.
103 | *
104 | * @static
105 | * @memberOf _
106 | * @since 0.1.0
107 | * @category Function
108 | * @param {Function} func The function to debounce.
109 | * @param {number} [wait=0] The number of milliseconds to delay.
110 | * @param {Object} [options={}] The options object.
111 | * @param {boolean} [options.leading=false]
112 | * Specify invoking on the leading edge of the timeout.
113 | * @param {number} [options.maxWait]
114 | * The maximum time `func` is allowed to be delayed before it's invoked.
115 | * @param {boolean} [options.trailing=true]
116 | * Specify invoking on the trailing edge of the timeout.
117 | * @returns {Function} Returns the new debounced function.
118 | * @example
119 | *
120 | * // Avoid costly calculations while the window size is in flux.
121 | * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
122 | *
123 | * // Invoke `sendMail` when clicked, debouncing subsequent calls.
124 | * jQuery(element).on('click', _.debounce(sendMail, 300, {
125 | * 'leading': true,
126 | * 'trailing': false
127 | * }));
128 | *
129 | * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
130 | * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
131 | * var source = new EventSource('/stream');
132 | * jQuery(source).on('message', debounced);
133 | *
134 | * // Cancel the trailing debounced invocation.
135 | * jQuery(window).on('popstate', debounced.cancel);
136 | */
137 | function debounce(func, wait, options) {
138 | var lastArgs,
139 | lastThis,
140 | maxWait,
141 | result,
142 | timerId,
143 | lastCallTime,
144 | lastInvokeTime = 0,
145 | leading = false,
146 | maxing = false,
147 | trailing = true;
148 |
149 | if (typeof func != 'function') {
150 | throw new TypeError(FUNC_ERROR_TEXT);
151 | }
152 | wait = toNumber(wait) || 0;
153 | if (isObject(options)) {
154 | leading = !!options.leading;
155 | maxing = 'maxWait' in options;
156 | maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
157 | trailing = 'trailing' in options ? !!options.trailing : trailing;
158 | }
159 |
160 | function invokeFunc(time) {
161 | var args = lastArgs,
162 | thisArg = lastThis;
163 |
164 | lastArgs = lastThis = undefined;
165 | lastInvokeTime = time;
166 | result = func.apply(thisArg, args);
167 | return result;
168 | }
169 |
170 | function leadingEdge(time) {
171 | // Reset any `maxWait` timer.
172 | lastInvokeTime = time;
173 | // Start the timer for the trailing edge.
174 | timerId = $timeout(timerExpired, wait);
175 | // Invoke the leading edge.
176 | return leading ? invokeFunc(time) : result;
177 | }
178 |
179 | function remainingWait(time) {
180 | var timeSinceLastCall = time - lastCallTime,
181 | timeSinceLastInvoke = time - lastInvokeTime,
182 | result = wait - timeSinceLastCall;
183 |
184 | return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
185 | }
186 |
187 | function shouldInvoke(time) {
188 | var timeSinceLastCall = time - lastCallTime,
189 | timeSinceLastInvoke = time - lastInvokeTime;
190 |
191 | // Either this is the first call, activity has stopped and we're at the
192 | // trailing edge, the system time has gone backwards and we're treating
193 | // it as the trailing edge, or we've hit the `maxWait` limit.
194 | return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
195 | (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
196 | }
197 |
198 | function timerExpired() {
199 | var time = now();
200 | if (shouldInvoke(time)) {
201 | return trailingEdge(time);
202 | }
203 | // Restart the timer.
204 | timerId = $timeout(timerExpired, remainingWait(time));
205 | }
206 |
207 | function trailingEdge(time) {
208 | timerId = undefined;
209 |
210 | // Only invoke if we have `lastArgs` which means `func` has been
211 | // debounced at least once.
212 | if (trailing && lastArgs) {
213 | return invokeFunc(time);
214 | }
215 | lastArgs = lastThis = undefined;
216 | return result;
217 | }
218 |
219 | function cancel() {
220 | if (timerId !== undefined) {
221 | $timeout.cancel(timerId);
222 | }
223 | lastInvokeTime = 0;
224 | lastArgs = lastCallTime = lastThis = timerId = undefined;
225 | }
226 |
227 | function flush() {
228 | return timerId === undefined ? result : trailingEdge(now());
229 | }
230 |
231 | function debounced() {
232 | var time = now(),
233 | isInvoking = shouldInvoke(time);
234 |
235 | lastArgs = arguments;
236 | lastThis = this;
237 | lastCallTime = time;
238 |
239 | if (isInvoking) {
240 | if (timerId === undefined) {
241 | return leadingEdge(lastCallTime);
242 | }
243 | if (maxing) {
244 | // Handle invocations in a tight loop.
245 | timerId = $timeout(timerExpired, wait);
246 | return invokeFunc(lastCallTime);
247 | }
248 | }
249 | if (timerId === undefined) {
250 | timerId = $timeout(timerExpired, wait);
251 | }
252 | return result;
253 | }
254 | debounced.cancel = cancel;
255 | debounced.flush = flush;
256 | return debounced;
257 | }
258 |
259 | /**
260 | * Checks if `value` is the
261 | * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
262 | * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
263 | *
264 | * @static
265 | * @memberOf _
266 | * @since 0.1.0
267 | * @category Lang
268 | * @param {*} value The value to check.
269 | * @returns {boolean} Returns `true` if `value` is an object, else `false`.
270 | * @example
271 | *
272 | * _.isObject({});
273 | * // => true
274 | *
275 | * _.isObject([1, 2, 3]);
276 | * // => true
277 | *
278 | * _.isObject(_.noop);
279 | * // => true
280 | *
281 | * _.isObject(null);
282 | * // => false
283 | */
284 | function isObject(value) {
285 | var type = typeof value;
286 | return !!value && (type == 'object' || type == 'function');
287 | }
288 |
289 | /**
290 | * Checks if `value` is object-like. A value is object-like if it's not `null`
291 | * and has a `typeof` result of "object".
292 | *
293 | * @static
294 | * @memberOf _
295 | * @since 4.0.0
296 | * @category Lang
297 | * @param {*} value The value to check.
298 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
299 | * @example
300 | *
301 | * _.isObjectLike({});
302 | * // => true
303 | *
304 | * _.isObjectLike([1, 2, 3]);
305 | * // => true
306 | *
307 | * _.isObjectLike(_.noop);
308 | * // => false
309 | *
310 | * _.isObjectLike(null);
311 | * // => false
312 | */
313 | function isObjectLike(value) {
314 | return !!value && typeof value == 'object';
315 | }
316 |
317 | /**
318 | * Checks if `value` is classified as a `Symbol` primitive or object.
319 | *
320 | * @static
321 | * @memberOf _
322 | * @since 4.0.0
323 | * @category Lang
324 | * @param {*} value The value to check.
325 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
326 | * @example
327 | *
328 | * _.isSymbol(Symbol.iterator);
329 | * // => true
330 | *
331 | * _.isSymbol('abc');
332 | * // => false
333 | */
334 | function isSymbol(value) {
335 | return typeof value == 'symbol' ||
336 | (isObjectLike(value) && objectToString.call(value) == symbolTag);
337 | }
338 |
339 | /**
340 | * Converts `value` to a number.
341 | *
342 | * @static
343 | * @memberOf _
344 | * @since 4.0.0
345 | * @category Lang
346 | * @param {*} value The value to process.
347 | * @returns {number} Returns the number.
348 | * @example
349 | *
350 | * _.toNumber(3.2);
351 | * // => 3.2
352 | *
353 | * _.toNumber(Number.MIN_VALUE);
354 | * // => 5e-324
355 | *
356 | * _.toNumber(Infinity);
357 | * // => Infinity
358 | *
359 | * _.toNumber('3.2');
360 | * // => 3.2
361 | */
362 | function toNumber(value) {
363 | if (typeof value == 'number') {
364 | return value;
365 | }
366 | if (isSymbol(value)) {
367 | return NAN;
368 | }
369 | if (isObject(value)) {
370 | var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
371 | value = isObject(other) ? (other + '') : other;
372 | }
373 | if (typeof value != 'string') {
374 | return value === 0 ? value : +value;
375 | }
376 | value = value.replace(reTrim, '');
377 | var isBinary = reIsBinary.test(value);
378 | return (isBinary || reIsOctal.test(value))
379 | ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
380 | : (reIsBadHex.test(value) ? NAN : +value);
381 | }
382 |
383 | return debounce;
384 |
385 | }]);
386 |
--------------------------------------------------------------------------------
/src/services/apMesaFilterFunctions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.services.apMesaFilterFunctions', [])
19 |
20 | .service('apMesaFilterFunctions', function() {
21 |
22 | function like(term, value) {
23 | term = term.toLowerCase().trim();
24 | value = String(value).toLowerCase();
25 | var first = term[0];
26 |
27 | // negate
28 | if (first === '!') {
29 | term = term.substr(1);
30 | if (term === '') {
31 | return true;
32 | }
33 | return value.indexOf(term) === -1;
34 | }
35 |
36 | // strict
37 | if (first === '=') {
38 | term = term.substr(1);
39 | return term === value.trim();
40 | }
41 |
42 | // remove escaping backslashes
43 | term = term.replace('\\!', '!');
44 | term = term.replace('\\=', '=');
45 |
46 | return value.indexOf(term) !== -1;
47 | }
48 |
49 | function likeFormatted(term, value, computedValue, row) {
50 | return like(term,computedValue,computedValue, row);
51 | }
52 | like.placeholder = likeFormatted.placeholder = 'string search';
53 | like.title = likeFormatted.title = 'Search by text, eg. "foo". Use "!" to exclude and "=" to match exact text, e.g. "!bar" or "=baz".';
54 |
55 | function number(term, value) {
56 | value = parseFloat(value);
57 | term = term.trim();
58 | var first_two = term.substr(0,2);
59 | var first_char = term[0];
60 | var against_1 = term.substr(1)*1;
61 | var against_2 = term.substr(2)*1;
62 | if ( first_two === '<=' ) {
63 | return value <= against_2 ;
64 | }
65 | if ( first_two === '>=' ) {
66 | return value >= against_2 ;
67 | }
68 | if ( first_char === '<' ) {
69 | return value < against_1 ;
70 | }
71 | if ( first_char === '>' ) {
72 | return value > against_1 ;
73 | }
74 | if ( first_char === '~' ) {
75 | return Math.round(value) === against_1 ;
76 | }
77 | if ( first_char === '=' ) {
78 | return against_1 === value ;
79 | }
80 | return value.toString().indexOf(term.toString()) > -1 ;
81 | }
82 | function numberFormatted(term, value, computedValue) {
83 | return number(term, computedValue);
84 | }
85 | number.placeholder = numberFormatted.placeholder = 'number search';
86 | number.title = numberFormatted.title = 'Search by number, e.g. "123". Optionally use comparator expressions like ">=10" or "<1000". Use "~" for approx. int values, eg. "~3" will match "3.2"';
87 |
88 |
89 | var unitmap = {};
90 | unitmap.second = unitmap.sec = unitmap.s = 1000;
91 | unitmap.minute = unitmap.min = unitmap.m = unitmap.second * 60;
92 | unitmap.hour = unitmap.hr = unitmap.h = unitmap.minute * 60;
93 | unitmap.day = unitmap.d = unitmap.hour * 24;
94 | unitmap.week = unitmap.wk = unitmap.w = unitmap.day * 7;
95 | unitmap.month = unitmap.week * 4;
96 | unitmap.year = unitmap.yr = unitmap.y = unitmap.day * 365;
97 |
98 | var clauseExp = /(\d+(?:\.\d+)?)\s*([a-z]+)/;
99 | function parseDateFilter(string) {
100 |
101 | // split on clauses (if any)
102 | var clauses = string.trim().split(',');
103 | var total = 0;
104 | // parse each clause
105 | for (var i = 0; i < clauses.length; i++) {
106 | var clause = clauses[i].trim();
107 | var terms = clauseExp.exec(clause);
108 | if (!terms) {
109 | continue;
110 | }
111 | var count = terms[1]*1;
112 | var unit = terms[2].replace(/s$/, '');
113 | if (! unitmap.hasOwnProperty(unit) ) {
114 | continue;
115 | }
116 | total += count * unitmap[unit];
117 | }
118 | return total;
119 |
120 | }
121 | function date(term, value) {
122 | // today
123 | // yesterday
124 | // 1 day ago
125 | // 2 days ago
126 |
127 | // < 1 day ago
128 | // < 10 minutes ago
129 | // < 10 min ago
130 | // < 10 minutes, 50 seconds ago
131 | // > 10 min, 30 sec ago
132 | // > 2 days ago
133 | // >= 1 day ago
134 | term = term.trim();
135 | if (!term) {
136 | return true;
137 | }
138 | value *= 1;
139 | var nowDate = new Date();
140 | var now = (+nowDate);
141 | var first_char = term[0];
142 | var other_chars = (term.substr(1)).trim();
143 | var lowerbound, upperbound;
144 | if ( first_char === '<' ) {
145 | lowerbound = now - parseDateFilter(other_chars);
146 | return value > lowerbound;
147 | }
148 | if ( first_char === '>' ) {
149 | upperbound = now - parseDateFilter(other_chars);
150 | return value < upperbound;
151 | }
152 |
153 | if ( term === 'today') {
154 | return new Date(value).toDateString() === nowDate.toDateString();
155 | }
156 |
157 | if ( term === 'yesterday') {
158 | return new Date(value).toDateString() === new Date(now - unitmap.d).toDateString();
159 | }
160 |
161 | var supposedDate = new Date(term);
162 | if (!isNaN(supposedDate)) {
163 | return new Date(value).toDateString() === supposedDate.toDateString();
164 | }
165 |
166 | return false;
167 | }
168 | date.placeholder = 'date search';
169 | date.title = 'Search by date. Enter a date string (RFC2822 or ISO 8601 date). You can also type "today", "yesterday", "> 2 days ago", "< 1 day 2 hours ago", etc.';
170 |
171 | return {
172 | like: like,
173 | likeFormatted: likeFormatted,
174 | number: number,
175 | numberFormatted: numberFormatted,
176 | date: date
177 | };
178 | });
179 |
--------------------------------------------------------------------------------
/src/services/apMesaFormatFunctions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.services.apMesaFormatFunctions', [])
19 |
20 | .service('apMesaFormatFunctions', function() {
21 | // TODO: add some default format functions
22 | return {};
23 | });
24 |
--------------------------------------------------------------------------------
/src/services/apMesaSortFunctions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | angular.module('apMesa.services.apMesaSortFunctions',[])
19 |
20 | .service('apMesaSortFunctions', function() {
21 | return {
22 | number: function(field){
23 | return function(row1,row2,options) {
24 | var val1, val2;
25 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) {
26 | val1 = options.getter(field, row1);
27 | val2 = options.getter(field, row2);
28 | }
29 | else {
30 | val1 = row1[field];
31 | val2 = row2[field];
32 | }
33 | return val1*1 - val2*1;
34 | };
35 | },
36 | string: function(field){
37 | return function(row1,row2,options) {
38 | var val1, val2;
39 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) {
40 | val1 = options.getter(field, row1);
41 | val2 = options.getter(field, row2);
42 | }
43 | else {
44 | val1 = row1[field];
45 | val2 = row2[field];
46 | }
47 | if(!val1 && val1 !== 0) {
48 | val1 = '';
49 | }
50 | if(!val2 && val2 !== 0) {
51 | val2 = '';
52 | }
53 | return val1.toString().toLowerCase().localeCompare(val2.toString().toLowerCase());
54 | };
55 | },
56 | stringFormatted: function(field){
57 | return function(row1,row2,options,column) {
58 | var val1, val2;
59 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) {
60 | val1 = options.getter(field, row1);
61 | val2 = options.getter(field, row2);
62 | }
63 | else {
64 | val1 = row1[field];
65 | val2 = row2[field];
66 | }
67 | val1 = column.format(val1, row1, column);
68 | val2 = column.format(val2, row2, column);
69 |
70 | return val1.toString().toLowerCase().localeCompare(val2.toString().toLowerCase());
71 | };
72 | },
73 | numberFormatted: function(field){
74 | return function(row1,row2,options,column) {
75 | var val1, val2;
76 | if (options !== undefined && {}.hasOwnProperty.call(options, 'getter')) {
77 | val1 = options.getter(field, row1);
78 | val2 = options.getter(field, row2);
79 | }
80 | else {
81 | val1 = row1[field];
82 | val2 = row2[field];
83 | }
84 | val1 = column.format(val1, row1, column);
85 | val2 = column.format(val2, row2, column);
86 |
87 | return val1*1 - val2*1;
88 | };
89 | },
90 | };
91 | });
92 |
--------------------------------------------------------------------------------
/src/templates/apMesa.tpl.html:
--------------------------------------------------------------------------------
1 |
7 |
85 |
86 |
87 |
88 |
89 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/templates/apMesaDummyRows.tpl.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andyperlitch/angularjs-table/23ff0603afd782df2e09c4b924f8fa12cbb82052/src/templates/apMesaDummyRows.tpl.html
--------------------------------------------------------------------------------
/src/templates/apMesaPaginationCtrls.tpl.html:
--------------------------------------------------------------------------------
1 |
13 |
14 | {{ options.rowsPerPageMessage }}
15 |
20 |
21 |
--------------------------------------------------------------------------------
/src/templates/apMesaRows.tpl.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/templates/apMesaStatusDisplay.tpl.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
{{ options.loadingText }}
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
{{ options.loadingErrorText }}
25 |
An error occurred.
26 |
27 |
28 |
29 |
30 |
31 |
{{ options.noRowsText }}
32 |
33 |
--------------------------------------------------------------------------------
/test/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": false,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "quotmark": "single",
15 | "regexp": true,
16 | "undef": true,
17 | "strict": true,
18 | "trailing": true,
19 | "smarttabs": true,
20 | "expr": true,
21 | "globals": {
22 | "after": false,
23 | "afterEach": false,
24 | "angular": false,
25 | "before": false,
26 | "beforeEach": false,
27 | "browser": false,
28 | "describe": false,
29 | "expect": false,
30 | "inject": false,
31 | "it": false,
32 | "sinon": false,
33 | "spyOn": false,
34 | "$": false
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/test/karma-e2e.conf.js:
--------------------------------------------------------------------------------
1 | var sharedConfig = require('./karma-shared.conf');
2 |
3 | module.exports = function(config) {
4 | var conf = sharedConfig();
5 |
6 | conf.files = conf.files.concat([
7 | //test files
8 | './test/e2e/**/*.js'
9 | ]);
10 |
11 | conf.proxies = {
12 | '/': 'http://localhost:9999/'
13 | };
14 |
15 | conf.urlRoot = '/__karma__/';
16 |
17 | conf.frameworks = ['ng-scenario'];
18 |
19 | config.set(conf);
20 | };
21 |
--------------------------------------------------------------------------------
/test/karma-midway.conf.js:
--------------------------------------------------------------------------------
1 | var sharedConfig = require('./karma-shared.conf');
2 |
3 | module.exports = function(config) {
4 | var conf = sharedConfig();
5 |
6 | conf.files = conf.files.concat([
7 | //extra testing code
8 | 'node_modules/ng-midway-tester/src/ngMidwayTester.js',
9 |
10 | //mocha stuff
11 | 'test/mocha.conf.js',
12 |
13 | //test files
14 | 'test/midway/appSpec.js',
15 | 'test/midway/controllers/controllersSpec.js',
16 | 'test/midway/filters/filtersSpec.js',
17 | 'test/midway/directives/directivesSpec.js',
18 | 'test/midway/requestsSpec.js',
19 | 'test/midway/routesSpec.js',
20 | 'test/midway/**/*.js'
21 | ]);
22 |
23 | conf.proxies = {
24 | '/': 'http://localhost:9999/'
25 | };
26 |
27 | config.set(conf);
28 | };
29 |
--------------------------------------------------------------------------------
/test/karma-shared.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var fs = require('fs');
5 | var index = fs.readFileSync(path.normalize(__dirname + '/../app/index.html'), 'utf8');
6 | var re = /src="bower_components[^"]+"/g;
7 | var bower_scripts = index.match(re).map(function(src) {
8 | return src.replace('src="','app/').replace('"','');
9 | });
10 |
11 | module.exports = function() {
12 | return {
13 | basePath: '../',
14 | frameworks: ['mocha','sinon-chai', 'sinon', 'chai'],
15 | browsers: ['PhantomJS'],
16 | autoWatch: true,
17 | reporters: ['dots', 'coverage'],
18 | // plugins: ['karma-chrome-launcher','karma-mocha','karma-coverage'],
19 |
20 | // tell karma how you want the coverage results
21 | coverageReporter: {
22 | type : 'html',
23 | // where to store the report
24 | dir : 'coverage/'
25 | },
26 |
27 | // these are default values anyway
28 | singleRun: false,
29 | colors: true,
30 |
31 | files : bower_scripts.concat([
32 |
33 | //App-specific Code
34 | 'src/**/*.js',
35 | 'app/scripts/app.js',
36 |
37 |
38 | //Test-Specific Code
39 | 'node_modules/chai/chai.js',
40 | 'node_modules/sinon/pkg/sinon.js',
41 | 'test/lib/chai-should.js',
42 | 'test/lib/chai-expect.js'
43 | ])
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/test/karma-unit.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var sharedConfig = require('./karma-shared.conf');
4 |
5 | module.exports = function(config) {
6 | var conf = sharedConfig();
7 |
8 | conf.files = conf.files.concat([
9 | //extra testing code
10 | 'app/bower_components/angular-mocks/angular-mocks.js',
11 |
12 | //mocha stuff
13 | // 'test/mocha.conf.js',
14 |
15 | //test files
16 | './test/spec/**/*.js',
17 |
18 | // template files
19 | 'src/templates/*.tpl.html'
20 | ]);
21 |
22 |
23 | conf.preprocessors = {
24 | // which html templates to be converted to js
25 | 'src/templates/*.tpl.html': ['ng-html2js'],
26 | // files we want to appear in the coverage report
27 | // 'src/**/*.js': ['coverage']
28 | };
29 |
30 | conf.ngHtml2JsPreprocessor = {
31 | // strip this from the file path
32 | stripPrefix: 'app/',
33 |
34 | // setting this option will create only a single module that contains templates
35 | // from all the files, so you can load them all with module('foo')
36 | moduleName: 'apMesa.templates'
37 | };
38 |
39 |
40 | config.set(conf);
41 | };
42 |
--------------------------------------------------------------------------------
/test/lib/chai-expect.js:
--------------------------------------------------------------------------------
1 | var expect = chai.expect;
--------------------------------------------------------------------------------
/test/lib/chai-should.js:
--------------------------------------------------------------------------------
1 | var should = chai.should;
--------------------------------------------------------------------------------
/test/runner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | End2end Test Runner
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/spec/directives/ap-mesa-cell.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Directive: ap-mesa-cell', function () {
4 |
5 | var element, scope, rootScope, isoScope, compile;
6 |
7 | beforeEach(function() {
8 | // define mock objects here
9 | });
10 |
11 | // load the directive's module
12 | beforeEach(module('apMesa', function($provide, $filterProvider) {
13 | // Inject dependencies like this:
14 | $filterProvider.register('commaGroups', function() {
15 | function commaGroups(value) {
16 | if (typeof value === 'undefined') {
17 | return '-';
18 | }
19 | var parts = value.toString().split('.');
20 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
21 | return parts.join('.');
22 | }
23 | return commaGroups;
24 | });
25 |
26 | }));
27 |
28 | beforeEach(inject(function ($compile, $rootScope) {
29 | // Cache these for reuse
30 | rootScope = $rootScope;
31 | compile = $compile;
32 |
33 | // Other setup, e.g. helper functions, etc.
34 |
35 | // Set up the outer scope
36 | scope = $rootScope.$new();
37 | scope.column = {
38 | id: 'id',
39 | key: 'id'
40 | };
41 | scope.row = {
42 | id: 'hello'
43 | };
44 | scope.options = {};
45 |
46 | }));
47 |
48 | afterEach(function() {
49 | // tear down here
50 | });
51 |
52 | describe('when a template is specified', function() {
53 |
54 | beforeEach(function() {
55 | scope.column.template = '{{ row[column.key] }} ';
56 | // Define and compile the element
57 | element = angular.element('
');
58 | element = compile(element)(scope);
59 | scope.$digest();
60 | isoScope = element.isolateScope();
61 | });
62 |
63 | it('should recompile the cell with the supplied template', function() {
64 | var strong = element.find('strong');
65 | expect(strong.length).to.equal(1);
66 | expect(strong.text()).to.equal(scope.row.id);
67 | });
68 |
69 | });
70 |
71 | describe('when a templateUrl is specified', function() {
72 | beforeEach(inject(function($templateCache) {
73 | scope.column.templateUrl = 'some/url.html';
74 | $templateCache.put(scope.column.templateUrl, '{{ row[column.key] }} ');
75 |
76 | // Define and compile the element
77 | element = angular.element('
');
78 | element = compile(element)(scope);
79 | scope.$digest();
80 | isoScope = element.isolateScope();
81 | }));
82 |
83 | it('should recompile the cell with the supplied templateUrl', function() {
84 | var strong = element.find('strong');
85 | expect(strong.length).to.equal(1);
86 | expect(strong.text()).to.equal(scope.row.id);
87 | });
88 | });
89 |
90 | describe('when an ngFilter is specified', function() {
91 | beforeEach(inject(function($templateCache) {
92 | scope.column.ngFilter = 'commaGroups';
93 | scope.row.id = '1000000';
94 | $templateCache.put(scope.column.templateUrl, '{{ row[column.key] }} ');
95 |
96 | // Define and compile the element
97 | element = angular.element('
');
98 | element = compile(element)(scope);
99 | scope.$digest();
100 | isoScope = element.isolateScope();
101 | }));
102 |
103 | it('should recompile the cell with the supplied filter', function() {
104 | var str = element.text();
105 | expect(str).to.equal('1,000,000');
106 | });
107 |
108 | afterEach(function() {
109 | delete scope.column.ngFilter;
110 | });
111 | });
112 |
113 | describe('when a getter is specified on options', function() {
114 | beforeEach(function() {
115 | scope.row.data = {
116 | id: 'hello2 in row.data'
117 | };
118 | scope.options.getter = function(key, row) {
119 | return row.data[key];
120 | };
121 | element = angular.element('
');
122 | element = compile(element)(scope);
123 | scope.$digest();
124 | });
125 |
126 | it('should use getter', function() {
127 | expect(element.text()).to.equal(scope.row.data.id);
128 | });
129 | });
130 |
131 | });
132 |
--------------------------------------------------------------------------------
/test/spec/directives/ap-mesa-selector.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Directive: apMesaSelector', function () {
4 |
5 | var element, scope, rootScope, isoScope, compile, sandbox, selected, row, column;
6 |
7 | beforeEach(function() {
8 | sandbox = sinon.sandbox.create();
9 |
10 | // define mock objects here
11 | });
12 |
13 | // load the directive's module
14 | beforeEach(module('apMesa', function($provide) {
15 | // Inject dependencies like this:
16 | // $provide.value('', mockThing);
17 |
18 | }));
19 |
20 | beforeEach(inject(function ($compile, $rootScope) {
21 | // Cache these for reuse
22 | rootScope = $rootScope;
23 | compile = $compile;
24 |
25 | // Other setup, e.g. helper functions, etc.
26 |
27 | // Set up the outer scope
28 | scope = $rootScope.$new();
29 | scope.selected = selected = [];
30 | scope.row = row = { id: 1 };
31 | scope.column = column = { key: 'id' };
32 |
33 | // Define and compile the element
34 | element = angular.element('
');
35 | element = compile(element)(scope);
36 | scope.$digest();
37 | isoScope = element.isolateScope();
38 | }));
39 |
40 | afterEach(function() {
41 | sandbox.restore();
42 | });
43 |
44 | describe('the click event', function() {
45 | var e;
46 | beforeEach(function() {
47 | e = $.Event('click');
48 | });
49 |
50 | it('should add row[column.key] to the selected array if it is not present already', function() {
51 | $(element).trigger(e);
52 | scope.$digest();
53 | expect(selected).to.contain(row[column.key]);
54 | });
55 |
56 | it('when "selectObject: true" is specified in column, should add the entire row(an object instead of a number/string) to the selected array if it is not present already', function() {
57 | column.selectObject = true;
58 | $(element).trigger(e);
59 | scope.$digest();
60 | for(var i = 0; i= 69; }
40 | if (term === 'short') { return value < 69; }
41 | return true;
42 | }
43 | feet_filter.title = filter_title = 'Type in "short" or "tall"';
44 | feet_filter.placeholder = filter_placeholder = '"short", "tall"';
45 |
46 | // Random data generator
47 | genRows = function(num){
48 | var retVal = [];
49 | for (var i=0; i < num; i++) {
50 | retVal.push(genRow(i));
51 | }
52 | return retVal;
53 | };
54 | function genRow(id){
55 |
56 | var fnames = ['joe','fred','frank','jim','mike','gary','aziz'];
57 | var lnames = ['sterling','smith','erickson','burke','ansari'];
58 | var seed = Math.random();
59 | var seed2 = Math.random();
60 | var first_name = fnames[ Math.round( seed * (fnames.length -1) ) ];
61 | var last_name = lnames[ Math.round( seed * (lnames.length -1) ) ];
62 |
63 | return {
64 | id: id,
65 | selected: false,
66 | first_name: first_name,
67 | last_name: last_name,
68 | age: Math.ceil(seed * 75) + 15,
69 | height: Math.round( seed2 * 36 ) + 48,
70 | weight: Math.round( seed2 * 130 ) + 90
71 | };
72 | }
73 |
74 | scope = $rootScope.$new();
75 | compile = $compile;
76 | timeout = $timeout;
77 |
78 | // Preload for labelTemplateUrl option
79 | $templateCache.put('example/th/template.html', 'Height! ');
80 |
81 | // Table columns
82 | scope.my_table_columns = columns = [
83 | {
84 | id: 'selector',
85 | key: 'selected',
86 | label: '',
87 | selector: true,
88 | width: '30px',
89 | lockWidth: true
90 | },
91 | {
92 | id: 'ID',
93 | key: 'id',
94 | sort: 'number',
95 | filter: 'number'
96 | },
97 | {
98 | id: 'first_name',
99 | key: 'first_name',
100 | label: 'First Name',
101 | sort: 'string',
102 | filter: 'like',
103 | title: 'First names are cool'
104 | },
105 | {
106 | id: 'last_name',
107 | key: 'last_name',
108 | label: 'Last Name',
109 | sort: 'string',
110 | filter: 'like',
111 | filter_placeholder: 'last name'
112 | },
113 | {
114 | id: 'age',
115 | key: 'age',
116 | sort: 'number',
117 | filter: 'number',
118 | labelTemplate: 'Age '
119 | },
120 | {
121 | id: 'height',
122 | key: 'height',
123 | label: 'Height',
124 | sort: 'number',
125 | filter: feet_filter,
126 | format: inches2feet,
127 | labelTemplateUrl: 'example/th/template.html'
128 | },
129 | {
130 | id: 'weight',
131 | key: 'weight',
132 | label: 'Weight',
133 | sort: 'number',
134 | filter: 'number',
135 | classes: 'test-classes-option'
136 | }
137 | ];
138 |
139 | // Table data
140 | scope.my_table_data = data = genRows(30);
141 |
142 | createElement = function() {
143 | element = angular.element(' ');
144 | element = compile(element)(scope);
145 | timeout.flush();
146 | scope.$digest();
147 | isoScope = element.isolateScope();
148 | // for (var k in isoScope) {
149 | // if (isoScope.hasOwnProperty(k)) {
150 | // // console.log('k: ', k, 'scope[k]', isoScope[k]);
151 | // console.log(k);
152 | // }
153 | // }
154 | };
155 |
156 | createElement();
157 | }));
158 |
159 | afterEach(function() {
160 | sandbox.restore();
161 | });
162 |
163 | it('should be okay with a delayed data set', function() {
164 | scope.my_table_data = undefined;
165 | // expect(createElement).not.to.throw();
166 | createElement();
167 | });
168 |
169 | it('should create two tables', function () {
170 | expect(element.find('table').length).to.equal(2);
171 | });
172 |
173 | it('should create an options object if one is not provided', function() {
174 | expect(isoScope.options).to.be.an('object');
175 | });
176 |
177 | it('should display the data passed to it', function () {
178 | var expected = data[0].first_name;
179 | var actual = element.find('table:eq(1) tbody.ap-mesa-rendered-rows tr:eq(0) td:eq(2)').text();
180 | actual = $.trim(actual);
181 | expect(actual).to.equal(expected);
182 | });
183 |
184 | it('should update displayed values when data has been updated', function() {
185 | scope.my_table_data = genRows(30);
186 | timeout.flush();
187 | scope.$apply();
188 | var expected = scope.my_table_data[0].first_name;
189 | var actual = element.find('table:eq(1) tbody.ap-mesa-rendered-rows tr:eq(0) td:eq(2)').text();
190 | actual = $.trim(actual);
191 | expect(actual).to.equal(expected);
192 | });
193 |
194 | it('should not throw if no columns array was found on the scope', inject(function($rootScope) {
195 | var scope2 = $rootScope.$new();
196 | scope2.rows = [];
197 | var el2 = angular.element(' ');
198 | var fn = function() {
199 | el2 = compile(el2)(scope2);
200 | scope.$digest();
201 | };
202 | expect(fn).not.to.throw();
203 | }));
204 |
205 | it('should allow an options object to be passed, and should use override default options', inject(function($rootScope) {
206 | var $scope2 = $rootScope.$new();
207 | $scope2.columns = [];
208 | $scope2.rows = [];
209 | $scope2.options = {
210 | bgSizeMultiplier: 3
211 | };
212 | var el2 = angular.element(' ');
213 | el2 = compile(el2)($scope2);
214 | $scope2.$digest();
215 | isoScope = el2.isolateScope();
216 | expect(isoScope.options.bgSizeMultiplier).to.equal(3);
217 | }));
218 |
219 | it('should attach a persistentState.searchTerms object to the scope', function() {
220 | expect(isoScope.persistentState.searchTerms).to.be.an('object');
221 | });
222 |
223 | it('should attach a sortOrder array to the scope', function() {
224 | expect(isoScope.persistentState.sortOrder).to.be.instanceof(Array);
225 | });
226 |
227 | it('options.getter should be a function', function() {
228 | // isoScope.options.getter = function() {
229 | // return 'valueFromGetter';
230 | // };
231 | if (isoScope.options !== undefined && {}.hasOwnProperty.call(isoScope.options, 'getter')) {
232 | expect(isoScope.options.getter).to.be.an('function');
233 | }
234 | });
235 |
236 | it('should set a default trackBy to "id"', function() {
237 | expect(isoScope.options.trackBy).to.equal('id');
238 | });
239 |
240 | describe('column header', function() {
241 |
242 | it('should have a .column-resizer element if lockWidth is not set', function() {
243 | expect(element.find('table:eq(0) th:eq(1) .column-resizer').length).to.equal(1);
244 | });
245 |
246 | it('should not have a .column-resizer element if lockWidth is set to true', function() {
247 | expect(element.find('table:eq(0) th:eq(0) .column-resizer').length).to.equal(0);
248 | });
249 |
250 | it('should set the style to column.width if supplied in column definition', function() {
251 | expect(element.find('table:eq(0) th:eq(0)').css('width')).to.equal(columns[0].width);
252 | });
253 |
254 | it('should display column.id if column.label is not specified', function() {
255 | var actual = $.trim(element.find('table:eq(0) th:eq(1) .column-text').text());
256 | expect(actual).to.equal(columns[1].id);
257 | });
258 |
259 | it('should display column.label if it is present', function() {
260 | var actual = $.trim(element.find('table:eq(0) th:eq(2) .column-text').text());
261 | expect(actual).to.equal(columns[2].label);
262 | });
263 |
264 | it('should display column.label if it is present, even if it is a falsey value', function() {
265 | var actual = $.trim(element.find('table:eq(0) th:eq(0) .column-text').text());
266 | expect(actual).to.equal(columns[0].label);
267 | });
268 |
269 | it('should attach a title (tooltip) to s where title was specified in column definition', function() {
270 | var actual = element.find('table:eq(0) th:eq(2)').attr('title');
271 | var expected = columns[2].title;
272 | expect(actual).to.equal(expected);
273 | });
274 |
275 | it('should have a col-header-{id} class on each ', function() {
276 | expect(element.find('table:eq(0) th:eq(2)').hasClass('table-header-first_name')).to.equal(true);
277 | });
278 |
279 | it('should have a "sortable-column" class on each whose column has a sort', function() {
280 | scope.my_table_columns.forEach(function(col, i) {
281 | if (col.sort) {
282 | expect(element.find('table:eq(0) th:eq(' + i + ')').hasClass('sortable-column')).to.equal(true);
283 | }
284 | });
285 | });
286 |
287 | it('should use the labelTemplate option', function() {
288 | expect(element.find('table:eq(0) th:eq(4) span.test-labelTemplate').length).to.equal(1);
289 | });
290 |
291 | it('should use the labelTemplateUrl option', function() {
292 | expect(element.find('table:eq(0) th:eq(5) span.test-labelTemplateUrl').length).to.equal(1);
293 | });
294 |
295 | it('should add any classes specified in the classes option to the header', function() {
296 | expect(element.find('table:eq(0) th:eq(6)').hasClass('test-classes-option')).to.equal(true);
297 | });
298 |
299 |
300 |
301 | });
302 |
303 | describe('column filter', function() {
304 |
305 | it('should have a placeholder if specified as a property on the filter function', function() {
306 | var actual = element.find('table:eq(0) tr:eq(1) td:eq(5) input').attr('placeholder');
307 | var expected = filter_placeholder;
308 | expect(actual).to.equal(expected);
309 | });
310 |
311 | it('should have a title if specified as a property on the filter function', function() {
312 | var actual = element.find('table:eq(0) tr:eq(1) td:eq(5) input').attr('title');
313 | var expected = filter_title;
314 | expect(actual).to.equal(expected);
315 | });
316 |
317 | });
318 |
319 |
320 | });
321 |
--------------------------------------------------------------------------------
/test/spec/filters/table-row-filter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Filter: apMesaRowFilter', function() {
4 |
5 | var columns, rows, persistentState, filter, fakeSearchFn1, fakeSearchFn2, sandbox, mockLog;
6 |
7 | beforeEach(function() {
8 | sandbox = sinon.sandbox.create();
9 | });
10 |
11 | // load the filter's module
12 | beforeEach(module('apMesa', function($provide) {
13 | mockLog = { warn: sandbox.spy() };
14 | $provide.value('$log', mockLog);
15 | }));
16 |
17 | beforeEach(inject(function($filter){
18 | filter = $filter('apMesaRowFilter');
19 |
20 | fakeSearchFn1 = function(term, value) {
21 | return value === term;
22 | };
23 |
24 | fakeSearchFn2 = function(term, value) {
25 | return value === 20;
26 | };
27 |
28 | columns = [
29 | { id: 'fname', key: 'fname', filter: fakeSearchFn1, format: sandbox.stub().returns('FORMATTED') },
30 | { id: 'col2', key: 'col2', filter: fakeSearchFn2 },
31 | { id: 'col3', key: 'col3', filter: 'like' },
32 | { id: 'col4', key: 'col4', filter: 'invalidFilterName' }
33 | ];
34 | rows = [
35 | { fname: 'phu', col2: 10, col3: 'foo' },
36 | { fname: 'amol', col2: 20, col3: 'foobar' },
37 | { fname: 'henry', col2: 30, col3: 'bar' }
38 | ];
39 | persistentState = {
40 | searchTerms: {},
41 | sortOrder: []
42 | };
43 | }));
44 |
45 | afterEach(function() {
46 | sandbox.restore();
47 | });
48 |
49 | it('should return all rows if no search terms are set', function() {
50 | expect(filter(rows, columns, persistentState, {})).to.equal(rows);
51 | });
52 |
53 | it('should ignore search terms that are empty strings or only whitespace', function() {
54 | ['', ' ', ' '].forEach(function(val) {
55 | persistentState.searchTerms.fname = val;
56 | expect(filter(rows, columns, persistentState, {})).to.equal(rows);
57 | });
58 | });
59 |
60 | it('should turn on a search function when a value is present in searchTerms', function() {
61 | persistentState.searchTerms.fname = 'phu';
62 | var results = filter(rows, columns, persistentState, {});
63 | expect( results.length ).to.equal(1);
64 | expect( results[0] ).to.equal(rows[0]);
65 |
66 | persistentState.searchTerms.fname = '';
67 | persistentState.searchTerms.col2 = 'some search';
68 | var results2 = filter(rows, columns, persistentState, {});
69 | expect( results2.length ).to.equal(1);
70 | expect( results2[0] ).to.equal(rows[1]);
71 | });
72 |
73 | it('should ignore invalid predefined filter names and call $log.warn', function() {
74 | persistentState.searchTerms.col4 = 'some search';
75 | var results = filter(rows, columns, persistentState, {});
76 | expect(results).to.equal(rows);
77 | expect(mockLog.warn).to.have.been.calledOnce;
78 | });
79 |
80 | it('should replace string references to built-in filter functions with actual functions', function() {
81 | persistentState.searchTerms.col3 = 'foo';
82 | filter(rows, columns, persistentState, {});
83 | expect(columns[2].filter).to.be.a('function');
84 | });
85 |
86 | it('should call the filter function with term, value, computedValue, and the row in that order', function() {
87 | // spy on filter fn
88 | sandbox.spy(columns[0], 'filter');
89 | persistentState.searchTerms.fname = 'test search';
90 | filter(rows, columns, persistentState, {});
91 | expect(columns[0].filter).to.have.been.calledWith('test search','phu','FORMATTED',rows[0]);
92 | expect(columns[0].filter).to.have.been.calledWith('test search','amol','FORMATTED',rows[1]);
93 | expect(columns[0].filter).to.have.been.calledWith('test search','henry','FORMATTED',rows[2]);
94 | });
95 |
96 | });
97 |
--------------------------------------------------------------------------------
/test/spec/filters/table-row-sorter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Filter: tableRowSorter', function() {
4 |
5 | var sandbox, sorter, columns, rows, numSort, numSort2, stringSort, sortOrder, transientState;
6 |
7 | beforeEach(module('apMesa'));
8 |
9 | beforeEach(inject(function(apMesaRowSorterFilter) {
10 | sandbox = sinon.sandbox.create();
11 |
12 | sorter = apMesaRowSorterFilter;
13 |
14 | stringSort = sandbox.spy(function(a,b) {
15 | return a.key1 < b.key1 ? -1 : a.key1 > b.key1 ? 1 : 0 ;
16 | });
17 | numSort = sandbox.spy(function(a,b) {
18 | return a.key2 - b.key2;
19 | });
20 | numSort2 = sandbox.spy(function(a,b) {
21 | return a.key3 - b.key3;
22 | });
23 |
24 | columns = [
25 | { id: 'key1', key: 'key1', sort: stringSort },
26 | { id: 'key2', key: 'key2', sort: numSort },
27 | { id: 'key3', key: 'key3', sort: numSort }
28 | ];
29 | rows = [
30 | { index: 0, key1:'c', key2:2, key3: 4 },
31 | { index: 1, key1:'b', key2:1, key3: 4 },
32 | { index: 2, key1:'a', key2:3, key3: 2 },
33 | { index: 3, key1:'b', key2:3, key3: 3 }
34 | ];
35 | transientState = {
36 | columnLookup: {}
37 | };
38 | columns.forEach(function(column) {
39 | transientState.columnLookup[column.id] = column;
40 | });
41 | }));
42 |
43 | afterEach(function() {
44 | sandbox.restore();
45 | });
46 |
47 | it('should be a function', function() {
48 | expect(sorter).to.be.a('function');
49 | });
50 |
51 | it('should return all rows if no sorting is active', function() {
52 | expect(sorter(rows,columns,[],{}, transientState)).to.equal(rows);
53 | });
54 |
55 | it('should sort ascending by a column whose "sorting" field is "+"', function() {
56 |
57 | sortOrder = [{id: 'key1', dir: '+'}];
58 |
59 | var result = sorter(rows, columns, sortOrder, {}, transientState);
60 | var idxs = result.map(function(r){ return r.index; });
61 | expect(idxs).to.eql([2,1,3,0]);
62 |
63 | });
64 |
65 | it('should sort descending by a column whose "sorting" field is "-"', function() {
66 |
67 | sortOrder = [{id:'key1', dir: '-'}];
68 |
69 | var result = sorter(rows,columns,sortOrder, {}, transientState);
70 | var idxs = result.map(function(r){ return r.index; });
71 | expect(idxs).to.eql([0,1,3,2]);
72 |
73 | });
74 |
75 | it('should ignore sort columns in sortOrder that do not exist', function() {
76 | sortOrder = [{id:'not_a_column', dir: '+'},{id:'key1', dir: '-'}];
77 |
78 | var result = sorter(rows,columns,sortOrder, {}, transientState);
79 | var idxs = result.map(function(r){ return r.index; });
80 | expect(idxs).to.eql([0,1,3,2]);
81 | });
82 |
83 | });
84 |
--------------------------------------------------------------------------------
/test/spec/services/table-format-functions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | describe('Service: apMesaFormatFunctions', function() {
3 |
4 | beforeEach(module('apMesa'));
5 |
6 | var formats;
7 |
8 | beforeEach(inject(['apMesaFormatFunctions', function(tableFormats) {
9 | formats = tableFormats;
10 | }]));
11 |
12 | it('should return an object containing functions', function() {
13 | expect(formats).to.be.an('object');
14 | angular.forEach(formats, function(fn) {
15 | expect(fn).to.be.a('function');
16 | });
17 | });
18 |
19 | });
20 |
--------------------------------------------------------------------------------
/test/spec/services/table-sort-functions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Service: tableSortFunctions', function() {
4 |
5 | var sandbox, sorts;
6 |
7 | beforeEach(function() {
8 | sandbox = sinon.sandbox.create();
9 | });
10 |
11 | beforeEach(module('apMesa'));
12 |
13 | beforeEach(inject(['apMesaSortFunctions', function(s) {
14 | sorts = s;
15 | }]));
16 |
17 | afterEach(function() {
18 | sandbox.restore();
19 | });
20 |
21 | it('should be an object', function() {
22 | expect(sorts).to.be.an('object');
23 | });
24 |
25 | describe('number sorting', function() {
26 |
27 | var factory, sorter;
28 |
29 | beforeEach(function() {
30 | factory = sorts.number;
31 | sorter = factory('fieldname');
32 | });
33 |
34 | it('should be a function', function() {
35 | expect(factory).to.be.a('function');
36 | });
37 |
38 | it('should return a function', function() {
39 | expect(sorter).to.be.a('function');
40 | });
41 |
42 | it('should return less than 0 if the value in first object is less than that of second', function() {
43 | expect( sorter( { fieldname: 1 }, { fieldname: 3 } ) ).to.be.below(0);
44 | });
45 |
46 | it('should return greater than 0 if the value in first object is more than that of second', function() {
47 | expect( sorter( { fieldname: 3 }, { fieldname: 1 } ) ).to.be.above(0);
48 | });
49 |
50 | it('should return 0 if values on both objects are the same', function() {
51 | expect( sorter( { fieldname: 5 }, { fieldname: 5 } ) ).to.equal(0);
52 | });
53 |
54 | it('should try and convert strings', function() {
55 | expect( sorter( { fieldname: '1' }, { fieldname: '3' } ) ).to.be.below(0);
56 | expect( sorter( { fieldname: '3' }, { fieldname: '1' } ) ).to.be.above(0);
57 | expect( sorter( { fieldname: '5' }, { fieldname: '5' } ) ).to.equal(0);
58 | });
59 |
60 | });
61 |
62 | describe('number formatted sorting', function() {
63 |
64 | var factory, sorter, pseudoColumn, lookup;
65 |
66 | beforeEach(function() {
67 | factory = sorts.numberFormatted;
68 | sorter = factory('fieldname');
69 | lookup = {
70 | 1: 10,
71 | 2: 9,
72 | 3: 8,
73 | 5: 11,
74 | 7: 11,
75 | 9: '1',
76 | 10: '2',
77 | 11: '3'
78 | };
79 | pseudoColumn = {
80 | format: function(raw) {
81 | return lookup[raw];
82 | }
83 | };
84 | });
85 |
86 | it('should be a function', function() {
87 | expect(factory).to.be.a('function');
88 | });
89 |
90 | it('should return a function', function() {
91 | expect(sorter).to.be.a('function');
92 | });
93 |
94 | it('should return less than 0 if the value in first object is less than that of second', function() {
95 | expect( sorter( { fieldname: 3 }, { fieldname: 1 }, {}, pseudoColumn ) ).to.be.below(0);
96 | expect( sorter( { fieldname: 3 }, { fieldname: 2 }, {}, pseudoColumn ) ).to.be.below(0);
97 | });
98 |
99 | it('should return greater than 0 if the value in first object is more than that of second', function() {
100 | expect( sorter( { fieldname: 1 }, { fieldname: 2 }, {}, pseudoColumn ) ).to.be.above(0);
101 | expect( sorter( { fieldname: 1 }, { fieldname: 3 }, {}, pseudoColumn ) ).to.be.above(0);
102 | });
103 |
104 | it('should return 0 if values on both objects are the same', function() {
105 | expect( sorter( { fieldname: 5 }, { fieldname: 7 }, {}, pseudoColumn ) ).to.equal(0);
106 | });
107 |
108 | it('should try and convert strings', function() {
109 | expect( sorter( { fieldname: '9' }, { fieldname: '10' }, {}, pseudoColumn ) ).to.be.below(0);
110 | expect( sorter( { fieldname: '11' }, { fieldname: '10' }, {}, pseudoColumn ) ).to.be.above(0);
111 | expect( sorter( { fieldname: '11' }, { fieldname: '11' }, {}, pseudoColumn ) ).to.equal(0);
112 | });
113 |
114 | });
115 |
116 | describe('string sorting', function() {
117 |
118 | var factory, sorter;
119 |
120 | beforeEach(function() {
121 | factory = sorts.string;
122 | sorter = factory('fieldname');
123 | });
124 |
125 | it('should be a function', function() {
126 | expect(factory).to.be.a('function');
127 | });
128 |
129 | it('should return a function', function() {
130 | expect(sorter).to.be.a('function');
131 | });
132 |
133 | it('should return less than 0 if the first value is alphabetically before the second value', function() {
134 | expect(sorter({fieldname: 'a'},{fieldname: 'b'})).to.be.below(0);
135 | });
136 |
137 | it('should return greater than 0 if the first value is alphabetically after the second value', function() {
138 | expect(sorter({fieldname: 'c'},{fieldname: 'b'})).to.be.above(0);
139 | });
140 |
141 | it('should return 0 if both are the same', function() {
142 | expect(sorter({fieldname: 'c'},{fieldname: 'c'})).to.equal(0);
143 | });
144 |
145 | it('should ignore case when comparing', function() {
146 | expect(sorter({fieldname: 'c'},{fieldname: 'C'})).to.equal(0);
147 | });
148 |
149 | });
150 |
151 | describe('string formatted sorting', function() {
152 |
153 | var factory, sorter, pseudoColumn, lookup;
154 |
155 | beforeEach(function() {
156 | factory = sorts.stringFormatted;
157 | sorter = factory('fieldname');
158 | lookup = {
159 | a: 'z',
160 | b: 'x',
161 | c: 'y',
162 | d: 'y'
163 | };
164 | pseudoColumn = {
165 | format: function(raw) {
166 | return lookup[raw];
167 | }
168 | };
169 | });
170 |
171 | it('should be a function', function() {
172 | expect(factory).to.be.a('function');
173 | });
174 |
175 | it('should return a function', function() {
176 | expect(sorter).to.be.a('function');
177 | });
178 |
179 | it('should return less than 0 if the first formatted value is alphabetically before the second formatted value', function() {
180 | expect(sorter({fieldname: 'b'},{fieldname: 'a'}, {}, pseudoColumn)).to.be.below(0);
181 | });
182 |
183 | it('should return greater than 0 if the first formatted value is alphabetically after the second formatted value', function() {
184 | expect(sorter({fieldname: 'a'},{fieldname: 'b'}, {}, pseudoColumn)).to.be.above(0);
185 | });
186 |
187 | it('should return 0 if both are the same', function() {
188 | expect(sorter({fieldname: 'c'},{fieldname: 'c'}, {}, pseudoColumn)).to.equal(0);
189 | });
190 |
191 | it('should ignore case when comparing', function() {
192 | expect(sorter({fieldname: 'c'},{fieldname: 'd'}, {}, pseudoColumn)).to.equal(0);
193 | });
194 |
195 | });
196 |
197 | });
198 |
--------------------------------------------------------------------------------
/update-gh-pages.sh:
--------------------------------------------------------------------------------
1 | # Updates the github pages
2 |
3 |
4 | # Reset the branch
5 | git checkout master
6 | git branch -D gh-pages
7 | git checkout -b gh-pages
8 |
9 | # Link demo files and create templates file
10 | for f in $(ls app); do ln -sf app/$f; done
11 | rm -f app/scripts/templates.js
12 | grunt html2js:dist
13 | mv dist/templates.js app/scripts/templates.js
14 |
15 | # Install bower stuff
16 | bower install -f
17 | ln -sf app/bower_components
18 |
19 | # Stage and commit
20 | git add app/bower_components -f
21 | git add -A
22 | COMMIT_ID=$(git rev-parse HEAD)
23 | git commit -m "gh-pages update for commit: ${COMMIT_ID}"
24 |
25 | # Push to remote
26 | git push --set-upstream origin gh-pages -f
27 | cp -r app/bower_components app/tmp_bower_components
28 | git checkout master
29 | rm -rf app/bower_components
30 | mv app/tmp_bower_components app/bower_components
31 | git clean -f
32 |
--------------------------------------------------------------------------------